Compare commits

..

1844 Commits

Author SHA1 Message Date
Vincent Koc
0249587a22 fix(android): remediate app CodeQL alerts 2026-04-28 01:17:58 -07:00
Peter Steinberger
db40ec404a fix: honor Ollama thinking catalog metadata 2026-04-28 09:15:28 +01:00
Peter Steinberger
67b16a4a6d fix: centralize source reply delivery mode 2026-04-28 09:14:19 +01:00
Peter Steinberger
1257e0e4ae ci: prepare qa channel boundary types 2026-04-28 09:13:49 +01:00
Peter Steinberger
4e921808d1 fix(line): persist inbound media in shared store 2026-04-28 09:12:11 +01:00
Peter Steinberger
fb3ea9efb1 fix: keep gateway model probes raw 2026-04-28 09:11:47 +01:00
Peter Steinberger
bce6c10290 fix: harden docs i18n prompt echoes 2026-04-28 09:11:28 +01:00
Peter Steinberger
725d557de6 fix(plugins): shorten runtime mirror lock hold 2026-04-28 09:10:37 +01:00
Peter Steinberger
0ef6702af3 build(android): update dependencies and lint config 2026-04-28 09:10:13 +01:00
Ayaan Zaidi
8da2fb1920 fix: seed claude-cli fallback context (#72069) (thanks @stainlu) 2026-04-28 13:35:59 +05:30
Ayaan Zaidi
5e4c29e9bc fix(agents): require claude fallback source provider 2026-04-28 13:35:59 +05:30
stainlu
4369c20bfe fix(agents): make originalProvider optional in runAgentAttempt params
The required-typed param introduced in 9987e7797f broke
attempt-execution.cli.test.ts and auth-profile-runtime-contract.test.ts
which construct runAgentAttempt params without an originalProvider field.
Make it optional and explicitly require the typeof check before passing
to isClaudeCliProvider so a missing field correctly skips the seed
(defensive default for fallback paths that didn't plumb the original
provider through, no-op for non-fallback paths).
2026-04-28 13:35:59 +05:30
stainlu
0bfcdcf044 fix(agents): scope claude-cli fallback seed and pair summary with boundary
Addresses review on #72069:

- Codex P1 ("Gate Claude prelude seeding by source provider"): the
  guard checked the *current* fallback candidate but not the failed
  attempt. A session that still carried a stale
  cliSessionBindings["claude-cli"] from an unrelated past run would
  inject Claude transcript context into a fallback chain that started
  on a different provider (e.g. openai -> openai-codex), leaking
  irrelevant prior conversation. Plumb `originalProvider` (the
  user-requested provider for the chain) through to runAgentAttempt
  and require `isClaudeCliProvider(originalProvider)` before reading
  Claude history.

- Codex P2 ("Prefer latest compact boundary when summary is missing"):
  the resolver always preferred the most recent explicit summary, so
  a later compaction without its own summary entry (rare crash case)
  paired stale summary text with post-latest-boundary turns. Restructure
  readClaudeCliFallbackSeed to queue summaries into pendingSummary and
  flush each boundary's pair atomically. A boundary with no preceding
  summary now correctly falls back to the boundary's own content
  rather than serving an older summary alongside fresh turns.

- Greptile P2 (newest-first break vs sparse coverage): the
  formatFallbackTurns walk intentionally stops on the first oversized
  turn so the prelude stays a contiguous "what was happening just
  before the failure" window. Document the design choice inline so a
  future maintainer doesn't reflexively change it to skip-and-continue.

Tests:
- New gateway cases for the boundary-without-summary edge case and
  for trailing summaries written without a paired boundary.
- existing 33 attempt-execution + 14 cli-session-history tests still
  pass; broader src/agents/command suite stays green (63/63).
2026-04-28 13:35:59 +05:30
stainlu
9691399e53 fix(agents): drop unnecessary non-null assertion in fallback prelude formatter
Local default oxlint did not run --type-aware so the warning was missed
on the initial commit; CI surfaced it via check-lint. Hoist the heading
into a named const so its length is read directly without the assertion.
2026-04-28 13:35:59 +05:30
stainlu
a96f1fa5ef fix(agents): seed claude-cli fallback prompts with prior-session context (#69973)
When a claude-cli attempt failed with a fallbackable error (e.g. a 402
billing limit), the next candidate -- typically a non-CLI provider --
ran with no prior conversation context. Claude Code keeps its own
JSONL session under ~/.claude/projects/, but the fallback runner only
sees what OpenClaw assembles from its own transcript, which is empty
for claude-cli sessions. The fallback model therefore behaved as if
the conversation just started, even though Claude later resumed fine.

Resolution mirrors what Claude Code itself does on resume after
compaction: prefer the explicit `/compact` summary, then append the
most recent post-boundary turns up to a char budget. Concretely:

- `readClaudeCliFallbackSeed` (gateway): walks the Claude JSONL with
  awareness of `type: "summary"` and `type: "system",
  subtype: "compact_boundary"` entries. Pre-boundary turns are dropped
  (they are represented by the summary); post-boundary turns become
  the recent-window. Multiple compactions are handled by preferring
  the latest summary. Path safety reuses the existing
  `resolveClaudeCliSessionFilePath` validation.

- `formatClaudeCliFallbackPrelude` / `buildClaudeCliFallbackContext\
Prelude` (agents helpers): format the harvested seed into a labeled
  prelude. Tool blocks are coalesced to compact "(tool call: name)" /
  "(tool result: …)" hints to keep the prompt budget honest. Newest
  turns are kept first when truncating; the summary is clearly
  labeled "(truncated)" if it overflows.

- `resolveFallbackRetryPrompt`: gains an optional
  `priorContextPrelude` that prepends before the existing retry
  marker. Empty/whitespace preludes are ignored; first-attempt prompts
  are unchanged.

- `runAgentAttempt`: builds the prelude when `isFallbackRetry === true`
  AND the new candidate is non-claude-cli AND a Claude-cli session
  binding is present. Same-provider fallbacks (claude-cli to
  claude-cli) are unaffected because Claude's own --resume still works.

Verified the new tests (12 in cli-session-history, 12 added to
attempt-execution) catch the regression: removing the prelude prepend
in resolveFallbackRetryPrompt makes both new prelude cases fail,
restoring the original cold-start behavior.

References:
- https://code.claude.com/docs/en/how-claude-code-works
- "Inside Claude Code: The Session File Format"
  https://databunny.medium.com/inside-claude-code-the-session-file-format-and-how-to-inspect-it-b9998e66d56b
2026-04-28 13:35:59 +05:30
Shakker
290c7ab848 test: add future strict startup benchmark case 2026-04-28 09:05:11 +01:00
Vincent Koc
dbab162abd ci: split codeql quality workflow (#73404) 2026-04-28 01:04:59 -07:00
Peter Steinberger
a811e164e3 ci: speed up full release validation 2026-04-28 09:02:57 +01:00
Peter Steinberger
c7af9c765c ci: tolerate missing clawsweeper dispatch access 2026-04-28 09:02:28 +01:00
Vincent Koc
a9a689ed2a fix(plugins): keep qa sdk aliases private 2026-04-28 01:01:19 -07:00
Peter Steinberger
f3191b7962 fix(agents): abort stalled Anthropic SSE reads 2026-04-28 09:00:37 +01:00
Peter Steinberger
a8b64b7d52 fix(doctor): require confirmation for transcript archive 2026-04-28 08:56:18 +01:00
Peter Steinberger
04e774eeac feat(android): add authenticated presence alive beacons (#73373)
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
2026-04-28 08:55:06 +01:00
Peter Steinberger
c788aa025e test: route session lifecycle test through fast lane 2026-04-28 08:52:20 +01:00
Peter Steinberger
2d575bc00e fix(onboarding): pin health auth during setup 2026-04-28 08:51:29 +01:00
Peter Steinberger
8b4a5d70e4 fix(build): preserve staged runtime deps on rebuild 2026-04-28 08:45:11 +01:00
Zhang Xiaofeng
a0900926c3 fix: add CJK error patterns to failover classification (#56242)
* fix: add CJK error patterns to failover classification

Chinese LLM providers (ZhipuAI/GLM, Bailian, Kimi/Moonshot, DeepSeek,
etc.) return error messages in Chinese. The existing failover
classification only matches English patterns, causing these errors to
fall through as unclassified — surfacing raw provider errors to users
instead of triggering model fallback.

Real production example: ZhipuAI error code 1234 returns
'网络错误,错误id:xxx,请联系客服。' (network error). This was not
matched by the existing 'network error' English pattern, so no failover
was triggered despite having a configured fallback model.

Changes:
- Add Chinese patterns to all error categories in failover-matches.ts:
  timeout, serverError, rateLimit, billing, auth, overloaded
- Add Chinese network error detection in formatTransportErrorCopy()
  for user-friendly error messages
- Add comprehensive test coverage for all CJK error categories

Follows the existing precedent set by Chinese context overflow patterns
in isContextOverflowError().

* fix: narrow billing pattern and fix placeholder issue URL

- Change '账户余额' to '账户余额不足' to avoid false positives on
  messages that merely mention account balance (per greptile review)
- Replace XXXXX placeholder with actual issue #56242

* fix: wire CJK auth failover patterns

* fix: classify CJK provider failover errors

* fix: place failover changelog entry in unreleased

---------

Co-authored-by: Altay <altay@uinaf.dev>
2026-04-28 10:44:17 +03:00
Peter Steinberger
47b6d3a334 test(video): isolate provider registry mocks 2026-04-28 08:43:20 +01:00
Peter Steinberger
f95f720b25 docs: separate mintlify list closings 2026-04-28 08:43:20 +01:00
Peter Steinberger
a30698166b fix(wizard): pin setup token for health check 2026-04-28 08:43:20 +01:00
Galin Iliev
274d05dfe7 fix(wizard): use setup token for onboarding health check
Fixes #72203

Co-authored-by: OpenClaw Bot <bot@openclaw.dev>
2026-04-28 08:43:20 +01:00
Scott Hanselman
146debf8c1 fix(tui): dedupe ASCII backspace events (#73335)
Merged via squash.

Prepared head SHA: 8f02f48acd
Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
Reviewed-by: @shanselman
2026-04-28 00:41:55 -07:00
Vincent Koc
0b82a7e718 test(ci): align main test expectations 2026-04-28 00:35:44 -07:00
Peter Steinberger
1dd011984a fix: add pricing bootstrap opt-out and sdk compat exports 2026-04-28 08:35:11 +01:00
Peter Steinberger
f5a7632ffc ci: allow legacy package stamp warnings 2026-04-28 08:31:16 +01:00
Peter Steinberger
b22926601f fix(ui): keep chat attachment payloads out of state 2026-04-28 08:27:53 +01:00
Peter Steinberger
bb7e8624ab fix: keep typing for group message-tool replies 2026-04-28 08:27:23 +01:00
Peter Steinberger
2f3e81fec2 ci: guard docs against poisoned tool text 2026-04-28 08:27:11 +01:00
Peter Steinberger
bcf4628092 ci: use gpt-5.5 for live OpenAI defaults 2026-04-28 08:27:11 +01:00
Peter Steinberger
39cecd6428 ci: avoid unnecessary docker image pulls 2026-04-28 08:24:29 +01:00
Peter Steinberger
04e96c11ea fix(gateway): skip plugin pricing scans when disabled 2026-04-28 08:23:53 +01:00
Peter Steinberger
2cfe8e17f5 test: type channel list plugin stubs 2026-04-28 08:21:35 +01:00
Peter Steinberger
438da9596e test: expand fast lane coverage 2026-04-28 08:19:40 +01:00
Peter Steinberger
78a12706ec fix(docs): make docs formatter mintlify-safe 2026-04-28 08:13:21 +01:00
Peter Steinberger
e4139c3cb6 fix(cli): show configured chat channels in list 2026-04-28 08:12:56 +01:00
Peter Steinberger
bdba90a20b feat: add authenticated iOS background presence beacon (#73330)
* feat: add iOS background presence beacon

Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>

* fix: keep iOS background reconnects ahead of beacon throttle

* build: refresh gateway protocol swift models

* fix: emit swift protocol string enums

---------

Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
2026-04-28 08:10:35 +01:00
Vincent Koc
d525d6486d fix(android): keep camera temp files private
Fix Android CodeQL local temp-file disclosure findings in camera capture.
2026-04-28 00:06:12 -07:00
Peter Steinberger
85fcf16804 ci: align docs formatter with mintlify guard 2026-04-28 08:06:03 +01:00
Peter Steinberger
12962dd883 fix(models): keep agent primaries strict 2026-04-28 08:01:42 +01:00
Peter Steinberger
cd1343c244 docs: fix heartbeat paramfield lists 2026-04-28 08:00:27 +01:00
Thatgfsj
3dff1272e9 fix: harden Windows gateway restart fallback (#69056)
Thanks @Thatgfsj.
2026-04-28 07:57:47 +01:00
Peter Steinberger
07c653e913 test: move pure hotspots to fast lane 2026-04-28 07:56:40 +01:00
Peter Steinberger
acea3f2465 fix(build): stamp runtime postbuild artifacts 2026-04-28 07:56:08 +01:00
Peter Steinberger
3256cf4fc7 docs: clarify group visible replies 2026-04-28 07:55:40 +01:00
Ayaan Zaidi
6b6a049337 fix: collapse nested runtime deps cache roots (#73205) (thanks @SymbolStar) 2026-04-28 12:25:25 +05:30
SymbolStar
dfaa06fe15 fix(bundled-runtime-deps): collapse nested cache pluginRoot to enclosing key
When a bundled plugin (e.g. plugin-sdk loaded transitively) is resolved via a
pluginRoot already inside the existing plugin-runtime-deps cache, its path
does not match the `dist/extensions/<plugin>` shape, so
resolveBundledPluginPackageRoot() returns null and the caller falls back to
the raw pluginRoot. resolveExistingExternalBundledRuntimeDepsRoots() then
rejected the path because the relative segment crossed a directory separator,
causing the resolver to mint a fresh `openclaw-unknown-<pathhash>` cache
beside the real versioned one. The two caches raced replaceNodeModulesDir()
and triggered ENOTEMPTY crash loops.

Treat any descendant of `<base>/openclaw-*` as belonging to that cache key
so nested resolutions return the existing versioned root instead of creating
a self-referential zombie cache.

Fixes #72956
2026-04-28 12:25:25 +05:30
Peter Steinberger
424560c6c2 docs: normalize mintlify component closings 2026-04-28 07:54:15 +01:00
Peter Steinberger
8831d2cf0a fix: normalize docs mintlify components 2026-04-28 07:52:17 +01:00
Peter Steinberger
fb40ed99a7 fix(sessions): remove session store rotation 2026-04-28 07:46:24 +01:00
Peter Steinberger
ad57a6d616 docs: replace reactions cache bust with prose 2026-04-28 07:37:14 +01:00
Peter Steinberger
df4d3fa5a9 fix(logging): redact subsystem console output before colorizing 2026-04-28 07:36:50 +01:00
edwin-rivera-dev
f2df49ab4b fix(logging): redact secrets at subsystem console sink (#73284)
createSubsystemLogger writes through writeConsoleLine, which intentionally
bypasses the patched console.* capture handler in src/logging/console.ts to
avoid recursion. That bypass also skipped the sink-boundary
redactSensitiveText() gate, so secrets reaching subsystem loggers as
message strings or formatted meta could appear verbatim on the terminal —
a follow-up to the file-transport redaction landed in #67953, tracked
under #64046.

Apply redactSensitiveText() at the writeConsoleLine() exit, immediately
after the existing Windows surrogate sanitization and before dispatching
to the rawConsole sink. This covers all subsystem console paths
(trace/debug/info/warn/error/fatal and .raw) because they share the same
writeConsoleLine() exit, matching the redact-at-sink-boundary pattern
already used in console.ts and the file transport.

Closes #73284
2026-04-28 07:36:50 +01:00
scoootscooob
3c636208b0 fix(messages): keep group replies tool-only by default
Rewrites the always-on reply handling so group/channel rooms default to message-tool-visible output, while `messages.groupChat.visibleReplies: \"automatic\"` preserves legacy auto-posting.\n\nThanks @scoootscooob.
2026-04-28 07:36:43 +01:00
Peter Steinberger
e388f289bf docs: refresh reactions source cache key 2026-04-28 07:36:13 +01:00
Ke Wang
a253660385 fix(gateway): accept heartbeat/cron/webhook channel hints in agent params (#73237) (#73282)
* fix(gateway): accept heartbeat/cron/webhook channel hints in agent params (#73237)

* test(gateway): cover internal reply channel hints

* test(openai): include codex mini catalog expectation

* test(openai): follow codex catalog fixture split

---------

Co-authored-by: Ke Wang <ke@pika.art>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-28 07:32:23 +01:00
Peter Steinberger
f321036a00 fix(acpx): tolerate wrapper chmod failures 2026-04-28 07:30:00 +01:00
darkamenosa
cb8b327488 fix(zalouser): persist refreshed session cookies
Persist refreshed `zca-js` session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local Zalo Personal session.

- Adds stable credential cookie signatures so equivalent cookie-jar reorderings do not rewrite credentials.
- Adds regression coverage for reordered live cookie jars preserving credential file content and mtime.
- Updates CHANGELOG.md: (#73277) Thanks @darkamenosa.

Co-authored-by: Tuyen <hxtxmu@gmail.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-28 14:26:37 +08:00
Shakker
577a540880 docs: note fireworks together catalog migration 2026-04-28 07:25:03 +01:00
Shakker
7b3d3ce361 feat: declare together model catalog 2026-04-28 07:25:03 +01:00
Shakker
1aa62c0b0a feat: declare fireworks model catalog 2026-04-28 07:25:03 +01:00
Peter Steinberger
c3c8d66acf test: align acp fast-lane routing assertions 2026-04-28 07:22:14 +01:00
Peter Steinberger
4e6c0965cb test: route acp runtime tests through fast lane 2026-04-28 07:17:02 +01:00
Peter Steinberger
84477e014d test(openai): align codex runtime fixture 2026-04-28 07:08:27 +01:00
Frank Yang
e008830d0e fix(agents): clean up local Claude stdio runs (#73292)
Clean up local Claude stdio one-shot runs before returning from embedded `openclaw agent --local`, including bundle MCP loopback teardown for local process resources.

Keeps gateway-owned MCP loopback cleanup internal to the Gateway, documents the local-vs-gateway behavior, and aligns the stale OpenAI provider-runtime fixture with the current unsupported Codex mini route.
2026-04-28 07:06:01 +01:00
Peter Steinberger
9b556291e9 test(openai): split codex catalog fixtures 2026-04-28 07:04:22 +01:00
Vincent Koc
1278f0bcc0 fix(codeql): tune Android pinning profile
Remove noisy missing-certificate-pinning query from the critical Android CodeQL profile; gateway TLS uses custom certificate fingerprint pinning.
2026-04-27 23:04:16 -07:00
Vincent Koc
5828dcdb05 test(gateway): reduce server shard memory pressure (#73317) 2026-04-27 22:58:15 -07:00
Peter Steinberger
870f7d1c0f test(openai): align codex mini contract 2026-04-28 06:56:29 +01:00
Peter Steinberger
b5371bfd63 fix(auth): migrate flat auth profiles in doctor 2026-04-28 06:53:48 +01:00
Peter Steinberger
2f2aee5fe8 ci: retry cross-os agent runtime deps staging 2026-04-28 06:51:05 +01:00
Peter Steinberger
4397717322 fix(telegram): report unauthorized startup tokens 2026-04-28 06:50:51 +01:00
Peter Steinberger
76a07b9a07 fix(cli): reject empty model run prompts 2026-04-28 06:50:44 +01:00
Peter Steinberger
ee75a8ec2c ci: document clawsweeper dispatch trigger 2026-04-28 06:50:33 +01:00
Peter Steinberger
9aa461747a fix(plugin-sdk): restore legacy root alias exports 2026-04-28 06:48:59 +01:00
Peter Steinberger
6f3674c8d0 ci: harden ClawSweeper dispatcher credentials 2026-04-28 06:48:38 +01:00
Peter Steinberger
6543c10ab6 test: route model catalog through fast lane 2026-04-28 06:48:29 +01:00
Peter Steinberger
ba17db96a4 ci: skip clawsweeper without app credentials 2026-04-28 06:48:29 +01:00
Peter Steinberger
0113248d91 fix(gateway): route text-only chat images to media understanding 2026-04-28 06:45:28 +01:00
Peter Steinberger
0fc1cdec45 ci: fix ClawSweeper dispatcher payload 2026-04-28 06:44:26 +01:00
Peter Steinberger
dc6031197b fix(models): hide unsupported codex mini route 2026-04-28 06:43:51 +01:00
Peter Steinberger
23818600bb ci: add ClawSweeper event dispatcher 2026-04-28 06:43:38 +01:00
Ke Wang
b4e9f1bd1c fix(memory-core): cap detached dream narratives (#73287)
Cap detached Dream Diary narrative subagent runs across cron dreaming sweeps so multi-workspace runs cannot fan out unbounded subagent sessions.

Adds regression coverage that queued detached narratives resume and clean up, plus a unit-fast lane correction for the security symlink audit test.
2026-04-28 06:42:07 +01:00
Peter Steinberger
89079a32ef refactor(memory-host): narrow runtime adapters 2026-04-28 06:40:37 +01:00
Vincent Koc
29a34e0a4d fix(android): use absolute logcat path
Fix Android CodeQL relative path command finding in debug log collection.
2026-04-27 22:40:00 -07:00
Peter Steinberger
59a4d7fb06 fix(telegram): normalize bot endpoint api roots 2026-04-28 06:36:38 +01:00
Vincent Koc
27e313053c test(gateway): keep session event suite minimal
Keep the session message websocket suite on the default minimal gateway harness to avoid full startup for event routing coverage.
2026-04-27 22:35:40 -07:00
Peter Steinberger
252cc7eccf test: fix unit-fast config assertion 2026-04-28 06:34:50 +01:00
Peter Steinberger
5916237962 fix(onboard): infer custom model image input 2026-04-28 06:34:16 +01:00
Shakker
d48c3e12a5 feat: gate legacy startup sidecar fallback 2026-04-28 06:31:55 +01:00
Peter Steinberger
583b419827 test(plugins): lock package boundary bridges 2026-04-28 06:30:44 +01:00
Peter Steinberger
833654586e fix(gateway): keep container restarts in-process 2026-04-28 06:30:12 +01:00
roytong9
a3fd97570f Normalize telegram topic targets in delivery resolution (#59069)
* Normalize telegram topic targets in delivery resolution

* fix(cron): preserve explicit Telegram topic targets

* fix(clownfish): address review for ghcrawl-165998-agentic-merge (1)

---------

Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-04-27 22:27:42 -07:00
Vincent Koc
9577703249 test(gateway): trim cron server memory hotspots
Move pure cron coverage off websocket server RPC loops and clean up timeout listener retention in gateway test helpers.
2026-04-27 22:26:41 -07:00
Vincent Koc
2c58c5d4ec fix(android): avoid trust-all TLS probing
Fix Android CodeQL insecure trust manager finding in gateway TLS probing.
2026-04-27 22:26:27 -07:00
Vincent Koc
ce01b8f250 fix(gateway): keep restart probe auth local (#72405)
* fix(gateway): keep restart probe auth local

* fix(gateway): repair local restart probe auth replacement
2026-04-27 22:25:20 -07:00
Vincent Koc
4c72e605cd fix(feishu): recover mojibake filenames from Content-Disposition (#72388) 2026-04-27 22:23:16 -07:00
Vincent Koc
d7e67b455a fix(tui): clear stale streaming after orphaned finals (#72389)
* fix(tui): clear stale streaming after orphaned finals

* fix(tui): clear stale streaming after orphaned finals

* fix(tui): clear stale streaming after orphaned finals
2026-04-27 22:23:13 -07:00
Shakker
db7cab4a9a fix: simplify volc catalog model builders 2026-04-28 06:21:24 +01:00
Shakker
37324dd112 docs: note byteplus volcengine catalog migration 2026-04-28 06:21:24 +01:00
Shakker
8a3252868f refactor: remove unused volc catalog sdk helper 2026-04-28 06:21:24 +01:00
Shakker
1cfa22acb1 feat: declare volcengine model catalog 2026-04-28 06:21:24 +01:00
Shakker
4513658f59 feat: declare byteplus model catalog 2026-04-28 06:21:24 +01:00
Peter Steinberger
25851e3cae fix(google-meet): harden observe mode speech health (#73256)
* fix(google-meet): harden observe mode speech health

* fix(google-meet): address observe speech review

* docs(google-meet): clarify observe mode guarantees
2026-04-28 06:21:10 +01:00
Jesse Merhi
2633b14914 feat(security): support operator-managed network proxy routing (#70044)
* feat: support operator-managed proxy routing

* docs: add network proxy changelog entry

* fix(proxy): restrict gateway bypass to loopback IPs

* fix(cli): harden container proxy URL checks

* docs(proxy): clarify gateway bypass scope

* docs: remove proxy changelog entry

* fix(proxy): clear startup CI guard failures

* fix(proxy): harden gateway proxy policy parsing

* fix(proxy): honor update shorthand proxy policy

* fix(cli): redact proxy URL suffixes

* test(proxy): keep gateway help off proxy startup

* fix(proxy): keep overlapping lifecycle active

* docs: add proxy changelog entry

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-04-28 00:20:47 -05:00
Peter Steinberger
025081dbc5 refactor(memory-host): consolidate core adapter 2026-04-28 06:20:19 +01:00
Peter Steinberger
82eb90b8a2 fix(agents): preserve trusted tool media metadata 2026-04-28 06:19:41 +01:00
Peter Steinberger
bb97f19396 fix(telegram): preserve streamed generated media 2026-04-28 06:19:41 +01:00
Peter Steinberger
8c8dfa768a refactor(models): share catalog capability lookup 2026-04-28 06:18:54 +01:00
Shakker
defddedbaf fix: carry plugin compat into loader reports 2026-04-28 06:18:46 +01:00
Shakker
d062f8130b feat: warn on implicit startup plugin compatibility 2026-04-28 06:18:45 +01:00
Peter Steinberger
f7e942f571 fix(tasks): ship task registry control runtime 2026-04-28 06:18:30 +01:00
Peter Steinberger
85bdaff418 test: route security audits through fast lane 2026-04-28 06:18:06 +01:00
teamclaw
057b8276cc fix(config): align in-process write sourceConfig with file-watcher (#73267)
Fix config writes so in-process reload notifications use the canonical post-write source snapshot, matching the file watcher path.

Adds regression coverage for the runtime source snapshot and changelog credit.
2026-04-28 06:16:58 +01:00
Peter Steinberger
a644e30245 fix(memory-core): retry unavailable dreaming model 2026-04-28 06:15:28 +01:00
Peter Steinberger
017b8db616 ci: speed up release validation shards 2026-04-28 06:14:23 +01:00
Peter Steinberger
3d53b39917 fix(gateway): honor configured vision models 2026-04-28 06:10:14 +01:00
Peter Steinberger
88bcb64681 test: route acp session mapper through fast lane 2026-04-28 06:10:03 +01:00
Peter Steinberger
526372ea36 fix(gateway): use runtime config for secret-backed talk
* fix(gateway): use runtime config for secret-backed talk

* test(gateway): relax talk config rpc timeout

* refactor(gateway): clarify talk config resolution
2026-04-28 06:05:27 +01:00
Vincent Koc
75deb12606 fix(gateway): avoid approval route config load
Avoid eager runtime config loading in the gateway approval path and unref approval cleanup grace timers.
2026-04-27 22:04:09 -07:00
Peter Steinberger
ece523a2b0 docs(plugin-sdk): refresh api baseline 2026-04-28 06:02:17 +01:00
Peter Steinberger
f7d139dfef refactor(memory-host): localize host utilities 2026-04-28 06:02:17 +01:00
Peter Steinberger
74a667f119 fix(telegram): retry startup control calls on fallback transport 2026-04-28 06:02:05 +01:00
Vincent Koc
c627afe1df fix(ci): restore plugin sdk browser config wrapper 2026-04-27 22:01:55 -07:00
Vincent Koc
2809630036 fix(android): disable app data backup (#73281) 2026-04-27 22:01:28 -07:00
Vincent Koc
7b18bd03bb fix(gateway): allow explicit loopback trusted proxy auth
Fixes #59167.

Supersedes #63379.
2026-04-27 22:01:06 -07:00
Peter Steinberger
1089e8b9e0 fix: stabilize memory host ci tests 2026-04-28 06:00:21 +01:00
Peter Steinberger
a6141a5a41 fix: harden macOS gateway updates 2026-04-28 05:58:05 +01:00
Peter Steinberger
66f80d1ed6 docs: avoid mdx list in sdk overview tip 2026-04-28 05:56:57 +01:00
samzong
25ef9c0c41 [Feat] Gateway: offload non-image attachments on chat.send (#67572)
Merged via squash.

Prepared head SHA: ecbd27fc30
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-28 12:55:00 +08:00
Peter Steinberger
a68cc94c36 fix: resolve main ci shard failures 2026-04-28 05:52:19 +01:00
Peter Steinberger
540cbe24be fix: allow memory flush model override 2026-04-28 05:50:55 +01:00
Peter Steinberger
dc3df62e67 refactor(memory-host): own package contract surface 2026-04-28 05:49:07 +01:00
Vincent Koc
6fadc56802 fix(media): tighten sanitizeMimeType anchoring (#73229)
* fix(media): tighten sanitizeMimeType anchoring

* fix(media): tighten sanitizeMimeType anchoring

* fix(media): tighten sanitizeMimeType anchoring
2026-04-27 21:48:36 -07:00
Gustavo Madeira Santana
d59f001507 test(qa-matrix): cover allowBots modes 2026-04-28 00:47:40 -04:00
Vincent Koc
6d7901f5c8 fix(acpx): lazy-load startup backend 2026-04-27 21:46:45 -07:00
Peter Steinberger
996818e6af fix: follow up main ci failures 2026-04-28 05:41:49 +01:00
Peter Steinberger
8a48994802 fix(otel): record liveness warnings 2026-04-28 05:41:30 +01:00
Peter Steinberger
66a0aa47e4 docs(google): clarify gemini 3.1 pro alias 2026-04-28 05:41:30 +01:00
Vincent Koc
2bce63cb65 fix(android): harden canvas webview bridge (#73240)
* fix(android): harden canvas webview bridge

* fix(android): make canvas content access hardening explicit

* fix(android): keep webview hardening inline for CodeQL

* fix(android): avoid webview getter false positive
2026-04-27 21:41:01 -07:00
Peter Steinberger
52daf5fbd3 fix(acpx): stage Claude ACP adapter runtime dependency 2026-04-28 05:38:15 +01:00
Peter Steinberger
59bd7e47e8 docs: avoid mdx lists inside callouts 2026-04-28 05:34:44 +01:00
Peter Steinberger
b8c44bfc82 fix: restore main ci and speed tests 2026-04-28 05:34:28 +01:00
Brian Newman
055127425f fix(export): fix broken template placeholders in session export HTML (#41861)
* fix(export): fix broken template placeholders in session export HTML

The {{MARKED_JS}}, {{HIGHLIGHT_JS}}, and {{JS}} placeholders in the
export HTML template were split across multiple lines by a code
formatter, turning them into JS block statements instead of template
tokens. The generateHtml() function uses .replace('{{MARKED_JS}}', ...)
which requires contiguous strings, so the vendor JS and app code were
never injected — producing a 2MB HTML file that opens with styles and
session data but renders blank (no JS to parse/display the data).

Fix: collapse placeholders to single-line {{TOKEN}} format and add
prettier-ignore comments to prevent re-formatting.

Introduced in 9d403fd.

* fix(export): use function replacers for vendor JS injection

String.replace() interprets $ sequences ($&, $$, $', etc.) in
replacement strings. The minified vendor libraries (highlight.min.js,
marked.min.js) and the template JS contain literal $ characters that
get mutated during injection — e.g. $& becomes the matched placeholder
text, $$ becomes a single $.

Fix: use arrow function replacers for JS content so replacement text
is injected verbatim without $ interpretation. CSS and session data
use string replacers since they don't contain problematic $ patterns.

Flagged by Codex review (P2).

* ci: retrigger checks

* fix(export-session): restore inline export scripts

---------

Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-04-27 21:34:20 -07:00
Peter Steinberger
5826774076 fix(diagnostics-otel): handle liveness warnings 2026-04-28 05:32:40 +01:00
Peter Steinberger
b60eb1711a refactor(plugin-sdk): add managed task flow runtime 2026-04-28 05:32:40 +01:00
Peter Steinberger
d987e153fe docs: fix plugin architecture mdx 2026-04-28 05:32:19 +01:00
Peter Steinberger
03e0f17069 docs(changelog): consolidate 2026.4.27 notes 2026-04-28 05:31:19 +01:00
Shakker
c77aead063 docs: refresh plugin sdk api baseline 2026-04-28 05:30:04 +01:00
Shakker
31e01eb286 fix: narrow stepfun manifest provider keys 2026-04-28 05:30:04 +01:00
Shakker
d76540ff30 docs: update manifest catalog migration note 2026-04-28 05:30:04 +01:00
Shakker
c242f0c35f feat: declare stepfun model catalogs 2026-04-28 05:30:04 +01:00
Shakker
b3dce79af1 feat: declare tencent tokenhub model catalog 2026-04-28 05:30:04 +01:00
Shakker
fd484cf472 refactor: build deepseek catalog from manifest 2026-04-28 05:30:04 +01:00
Shakker
a4eb89c809 refactor: build moonshot catalog from manifest 2026-04-28 05:30:04 +01:00
Shakker
68a1dfb7e3 docs: document manifest provider catalog sdk helper 2026-04-28 05:30:04 +01:00
Shakker
a3ad2723cc fix: fail on dropped manifest catalog rows 2026-04-28 05:30:04 +01:00
Shakker
4168575b88 docs: note manifest provider catalog helper 2026-04-28 05:30:04 +01:00
Shakker
2d8ee0452e fix: normalize raw manifest provider catalogs 2026-04-28 05:30:04 +01:00
Shakker
a047144660 fix: narrow manifest catalog runtime inputs 2026-04-28 05:30:04 +01:00
Shakker
a36aeac072 fix: reject incomplete manifest provider catalogs 2026-04-28 05:30:04 +01:00
Shakker
129d5be507 refactor: build cerebras and mistral catalogs from manifests 2026-04-28 05:30:04 +01:00
Shakker
1f883f3dff refactor: build nvidia catalog from manifest 2026-04-28 05:30:04 +01:00
Shakker
833dcccddf refactor: build qianfan and xiaomi catalogs from manifests 2026-04-28 05:30:04 +01:00
Shakker
5cba55e520 feat: add manifest provider catalog helper 2026-04-28 05:30:04 +01:00
Peter Steinberger
1267a14326 docs: fix plugin architecture mdx 2026-04-28 05:29:26 +01:00
Peter Steinberger
cb1bca1a16 fix(diagnostics): export liveness warning telemetry 2026-04-28 05:28:04 +01:00
Peter Steinberger
001bf47727 chore(release): open 2026.4.27 development 2026-04-28 05:28:04 +01:00
Peter Steinberger
548f946ffd test(macos): remove conflict marker 2026-04-28 05:28:04 +01:00
Peter Steinberger
5dec95f35c test(macos): stabilize gateway control test 2026-04-28 05:28:04 +01:00
Peter Steinberger
35c9dd06b2 fix(cli): respect replace mode in model picker 2026-04-28 05:26:25 +01:00
Peter Steinberger
1a2f60c0a1 chore(browser): remove old security mock path 2026-04-28 05:21:58 +01:00
Peter Steinberger
af7f651db3 refactor(plugin-sdk): retire reserved helper exports 2026-04-28 05:21:57 +01:00
Peter Steinberger
870d993eb8 fix(ui): request configured model list 2026-04-28 05:21:08 +01:00
Peter Steinberger
000d52be37 ci: pin Google live gateway profile models 2026-04-28 05:19:33 +01:00
Vincent Koc
e8b4e39a97 fix(gateway): clear fallback context on close
Fixes gateway fallback request context cleanup on close/startup failure and shards the full gateway Vitest lane to avoid the observed memory hang.\n\nValidation:\n- Testbox: OPENCLAW_TESTBOX=1 pnpm check:changed\n- Testbox: env OPENCLAW_VITEST_MAX_WORKERS=1 /usr/bin/time -v pnpm test:gateway (254 files, 2950 tests, max RSS 4144692 KB)
2026-04-27 21:19:21 -07:00
Peter Steinberger
738f5f7508 fix: prevent channel login exec wedges 2026-04-28 05:16:43 +01:00
Peter Steinberger
ed98762832 fix: seed docs i18n codex auth 2026-04-28 05:15:38 +01:00
Peter Steinberger
843980e173 test: route more fast specs through unit-fast 2026-04-28 05:14:15 +01:00
Peter Steinberger
ab95812d65 fix: record model fallback steps in trajectories 2026-04-28 05:08:34 +01:00
Peter Steinberger
714f3b59cc fix: preserve unknown compaction failure detail 2026-04-28 05:08:34 +01:00
Shakker
34a0a9fd06 chore: benchmark startup-lazy plugins 2026-04-28 05:08:14 +01:00
Omar Shahine
4b760be1dd fix(gateway): strip SecretRef secret inputs from messages.tts.providers before talk.config hands them to speech providers (#73111)
Closes the gap left by #72496 on the parallel `messages.tts.providers.<id>` site. After #72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from #72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes #73109. Refs #72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:06:28 -07:00
Peter Steinberger
97f3e6d3c2 fix: keep docs i18n codex home out of tmp 2026-04-28 05:05:06 +01:00
Peter Steinberger
1e3ce10e27 refactor(plugin-sdk): remove unused reserved helper exports 2026-04-28 05:00:53 +01:00
Peter Steinberger
4d69f81a4e fix: isolate docs i18n codex home 2026-04-28 04:58:51 +01:00
Peter Steinberger
da773cf074 test: fix startup recovery model fixture types 2026-04-28 04:57:57 +01:00
Peter Steinberger
d9a6dd0c36 ci: pin OpenAI live gateway profile model 2026-04-28 04:57:48 +01:00
Vincent Koc
9a19d8b8ea fix(bonjour): classify ciao IPv4 changed assertion
Classify ciao's IPv4 address changed assertion spelling in the Bonjour plugin and cover the exact upstream message.
2026-04-27 20:56:43 -07:00
Peter Steinberger
f6c0aa256e Revert "fix: use API-supported docs i18n model"
This reverts commit d6d0506135.
2026-04-28 04:55:06 +01:00
Peter Steinberger
fed337b164 test: speed media runtime specs 2026-04-28 04:53:57 +01:00
Vincent Koc
6f38425e5c security(gateway): route hook completion events to target agent session (#73228) 2026-04-27 20:53:52 -07:00
Peter Steinberger
0f64887623 test(gateway): preserve startup model allowlist 2026-04-28 04:53:32 +01:00
Peter Steinberger
d6d0506135 fix: use API-supported docs i18n model 2026-04-28 04:53:22 +01:00
Vincent Koc
42de56cc22 fix(ci): trust live docker harness scripts 2026-04-27 20:52:37 -07:00
Peter Steinberger
76d279fe10 docs: note gateway restart version wait 2026-04-28 04:46:32 +01:00
pickaxe
b46ff081f7 Wait for gateway version during restart 2026-04-28 04:46:32 +01:00
SymbolStar
f53ec52e7d fix(bonjour): raise stuck announcing threshold
Raise the Bonjour stuck-announcing watchdog threshold from 8s to 20s and align watchdog timer coverage so healthy 12-13s LAN announcements do not trigger false-positive advertiser teardown.
2026-04-27 20:44:08 -07:00
Peter Steinberger
c17b9fe623 chore(plugins): add SDK retirement plan report 2026-04-28 04:42:55 +01:00
Peter Steinberger
1df48506a7 test: accept codex agent model list 2026-04-28 04:42:07 +01:00
Peter Steinberger
093dba3806 fix(acpx): bundle Codex ACP adapter 2026-04-28 04:39:41 +01:00
Peter Steinberger
4fb543796b refactor(plugin-sdk): annotate dormant reserved subpaths 2026-04-28 04:39:33 +01:00
Peter Steinberger
0ff60d162c test: type heartbeat overflow model fixtures 2026-04-28 04:39:00 +01:00
Vincent Koc
b1439ca527 fix(ci): keep codex live harness helpers trusted 2026-04-27 20:37:57 -07:00
Peter Steinberger
4eb8a7d586 test: align run main commander mock 2026-04-28 04:34:41 +01:00
Peter Steinberger
995b51d309 test: fix qr cli runtime mock hoisting 2026-04-28 04:34:41 +01:00
Doncic
bf60e3ed31 fix(test): resolve vitest mock hoisting in qr-cli.test.ts 2026-04-28 04:34:41 +01:00
Peter Steinberger
0bdc1d0375 ci: hydrate provider env for testbox commands 2026-04-28 04:34:21 +01:00
Peter Steinberger
2860592302 fix(discord): hand off interactions asynchronously 2026-04-28 04:33:57 +01:00
Shakker
6f13982212 test: assert bundled startup plan metadata 2026-04-28 04:33:48 +01:00
Shakker
08cc44b57d feat: lazily load tool result middleware plugins 2026-04-28 04:33:47 +01:00
Shakker
fc3b8ad3ee fix: startup load skill workshop hooks 2026-04-28 04:33:47 +01:00
Shakker
c7b1f1285f test: fix bundled startup guard typing 2026-04-28 04:33:47 +01:00
Shakker
61ddddbe0f test: require bundled startup activation metadata 2026-04-28 04:33:47 +01:00
Shakker
86bdeb0561 perf: mark capability plugins startup lazy 2026-04-28 04:33:47 +01:00
Shakker
97016fbf02 perf: mark channel plugins startup lazy 2026-04-28 04:33:47 +01:00
Shakker
00d2c34889 perf: mark provider plugins startup lazy 2026-04-28 04:33:47 +01:00
Shakker
f1aaa2cd91 feat: declare startup plugin imports explicitly 2026-04-28 04:33:47 +01:00
Peter Steinberger
3945193257 fix: use codex for docs i18n 2026-04-28 04:33:41 +01:00
Peter Steinberger
b2d102109b fix(telegram): retry webhook registration failures 2026-04-28 04:33:22 +01:00
Peter Steinberger
5a2e5446a4 fix: explain heartbeat model bleed overflows 2026-04-28 04:32:55 +01:00
Peter Steinberger
68561a8c94 ci: use trusted codex live harness 2026-04-28 04:29:35 +01:00
Peter Steinberger
dfc14d1653 test: accept current codex status wording 2026-04-28 04:27:29 +01:00
Peter Steinberger
6c0cdf43e4 fix: honor subagent spawn model overrides 2026-04-28 04:25:31 +01:00
Peter Steinberger
e7495e2d92 ci: pass provider secrets to testbox 2026-04-28 04:24:15 +01:00
Peter Steinberger
38ba27834d chore: harden plugin boundary report 2026-04-28 04:23:53 +01:00
Peter Steinberger
073b3fbf88 test: move more runtime specs to fast lane 2026-04-28 04:23:48 +01:00
Vincent Koc
c205577f2c fix(cli): keep gateway run on fast path 2026-04-27 20:22:52 -07:00
Peter Steinberger
758262e1e3 test: keep live shard release partition unique 2026-04-28 04:20:49 +01:00
Peter Steinberger
379c43c754 test: align compatibility guard expectations 2026-04-28 04:20:49 +01:00
Peter Steinberger
070e2427bf test: remove stale root test helper bridges 2026-04-28 04:20:49 +01:00
Peter Steinberger
dab0e57914 style: format sdk helper imports 2026-04-28 04:20:49 +01:00
Peter Steinberger
896b82f430 test: align sdk helper imports 2026-04-28 04:20:49 +01:00
Peter Steinberger
aa6417b93d test: align doctor plugin manifest mocks 2026-04-28 04:20:49 +01:00
Peter Steinberger
993fee4066 fix(agents): avoid empty Anthropic tool result blocks 2026-04-28 04:20:49 +01:00
Vincent Koc
4102f8d28d fix(macos): parse model catalog without JavaScriptCore
Replaces JavaScriptCore catalog evaluation with a bounded fail-closed object-literal parser for the generated macOS model catalog.\n\nValidation: macos-node, macos-swift, security-fast, security-scm-fast, security-dependency-audit, workflow sanity checks passed on PR #73112.
2026-04-27 20:16:51 -07:00
Peter Steinberger
4b4cde7187 fix(memory): back off qmd open failures 2026-04-28 04:16:25 +01:00
Peter Steinberger
4db4d8976d ci: run release validation with trusted harness 2026-04-28 04:14:09 +01:00
Peter Steinberger
343f2d7245 fix: fail closed for invalid cron payload models 2026-04-28 04:12:54 +01:00
Peter Steinberger
00e30ba8d9 chore: add plugin boundary report 2026-04-28 04:12:30 +01:00
Gustavo Madeira Santana
ae616777f3 test(qa-matrix): cover approval metadata scenarios 2026-04-27 23:10:51 -04:00
Gustavo Madeira Santana
795e58acf2 test(matrix): cover approval metadata delivery 2026-04-27 23:10:51 -04:00
Peter Steinberger
b1a36226b1 test: stabilize faster unit lanes 2026-04-28 04:09:41 +01:00
Peter Steinberger
e11eb03182 fix: exclude plugin dependencies from backups 2026-04-28 04:03:20 +01:00
Peter Steinberger
719ec4f292 refactor: share OpenAI-compatible image provider 2026-04-28 04:01:43 +01:00
Peter Steinberger
358579b136 test: guard extension test api exports 2026-04-28 04:00:00 +01:00
Peter Steinberger
a812b8f919 test: use public plugin sdk test fixtures 2026-04-28 03:52:38 +01:00
Peter Steinberger
518d568de5 test: cover staged bundled facade deps 2026-04-28 03:52:24 +01:00
Peter Steinberger
129b996a4e refactor: tighten extension test support boundaries 2026-04-28 03:52:19 +01:00
Peter Steinberger
e5452a9c57 ci: speed up release validation 2026-04-28 03:52:05 +01:00
Peter Steinberger
f549703bed test: route more safe files to unit fast 2026-04-28 03:47:31 +01:00
Peter Steinberger
e9611e74a1 test: fix core support boundary helpers 2026-04-28 03:47:31 +01:00
Peter Steinberger
07494a43fc chore(release): publish 2026.4.26 appcast 2026-04-28 03:47:20 +01:00
Peter Steinberger
65b605569b docs: record release tweet workflow 2026-04-28 03:47:20 +01:00
Peter Steinberger
fc0a2bc87d fix: show banner on gateway fast path 2026-04-28 03:46:05 +01:00
Peter Steinberger
cfca2d4051 refactor: move remaining agent test contract files 2026-04-28 03:40:57 +01:00
Peter Steinberger
2628326264 refactor: expose agent runtime test contracts 2026-04-28 03:40:57 +01:00
Peter Steinberger
c1c9f5f1a3 test: speed up unit fast lane 2026-04-28 03:37:14 +01:00
Peter Steinberger
09a2ffc47a fix: prepare public artifact runtime deps 2026-04-28 03:34:53 +01:00
Peter Steinberger
35685e9960 refactor: centralize plugin gateway message dispatch 2026-04-28 03:28:51 +01:00
Peter Steinberger
7bf08e7344 refactor: move remaining SDK test helper files 2026-04-28 03:28:17 +01:00
Peter Steinberger
e1acb61317 refactor: expose SDK test helper subpaths 2026-04-28 03:28:17 +01:00
Shakker
21528222c3 docs: note static provider catalog manifests 2026-04-28 03:26:57 +01:00
Shakker
a30632eb28 feat: declare cerebras and mistral model catalogs 2026-04-28 03:26:57 +01:00
Shakker
7f87593548 feat: declare nvidia model catalog 2026-04-28 03:26:57 +01:00
Shakker
2d7b16e0db feat: declare qianfan and xiaomi model catalogs 2026-04-28 03:26:57 +01:00
Peter Steinberger
88068b9649 fix: prepare bundled facade runtime deps 2026-04-28 03:25:01 +01:00
Peter Steinberger
4a54682275 fix: tolerate stale plugin index channel metadata 2026-04-28 03:23:45 +01:00
Peter Steinberger
28f88ab2cc test: align extension contracts with dependency refresh 2026-04-28 03:16:12 +01:00
Shakker
13987b726a docs: show explicit startup activation in plugin examples 2026-04-28 03:13:20 +01:00
Shakker
72c4854fa0 docs: document plugin startup activation 2026-04-28 03:13:20 +01:00
Shakker
7754158292 perf: skip explicit startup opt out plugins 2026-04-28 03:13:20 +01:00
Shakker
5d52233c25 refactor: mark implicit startup sidecars deprecated 2026-04-28 03:13:20 +01:00
Shakker
b16fe2b229 feat: add plugin startup activation metadata 2026-04-28 03:13:20 +01:00
Peter Steinberger
a0a0ab4d9e fix(memory): resolve custom embedding provider ids 2026-04-28 03:11:19 +01:00
Peter Steinberger
632b0fd580 chore: update workspace dependencies 2026-04-28 03:09:44 +01:00
Peter Steinberger
bbed4ac096 test: stabilize and speed unit fast lane 2026-04-28 03:08:02 +01:00
Peter Steinberger
0835f9409a fix: route telegram cli sends through gateway 2026-04-28 03:01:22 +01:00
Peter Steinberger
662d5de746 docs: document QQBot groups and Yuanbao 2026-04-28 02:59:36 +01:00
Peter Steinberger
554f36b197 test(release): stabilize release validation waits
(cherry picked from commit a4266be808)
2026-04-28 02:59:34 +01:00
Peter Steinberger
8123db644b fix: break plugin command spec import cycle
(cherry picked from commit ced0e96cf2)
2026-04-28 02:59:16 +01:00
Shakker
197f95c94d docs: clarify refreshable model catalog authority 2026-04-28 02:59:07 +01:00
Shakker
2c1be64d97 fix: keep refreshable manifest catalogs registry backed 2026-04-28 02:59:07 +01:00
Shakker
5280b157f6 feat: declare chutes and kilocode model catalogs 2026-04-28 02:59:07 +01:00
Shakker
973a3226f0 fix: use refreshable manifest rows for provider list fast paths 2026-04-28 02:59:07 +01:00
Shakker
27a8875241 fix: append filtered registry rows in broad model lists 2026-04-28 02:59:07 +01:00
Shakker
53b53ba06b feat: declare refreshable model catalog supplements 2026-04-28 02:59:07 +01:00
Shakker
7231fcfec3 fix: avoid broad runtime catalog supplements 2026-04-28 02:59:06 +01:00
Shakker
8ac10cf164 refactor: support refreshable manifest list rows 2026-04-28 02:59:06 +01:00
Shakker
a0608af2ee docs: note broad model list normalization skip 2026-04-28 02:59:06 +01:00
Shakker
9682f3937e fix: skip runtime normalization for broad model lists 2026-04-28 02:59:06 +01:00
Shakker
8f92239fdb docs: note models list supplement speedup 2026-04-28 02:59:06 +01:00
Shakker
177da2c5a8 fix: skip resolved duplicate catalog supplements 2026-04-28 02:59:06 +01:00
Shakker
495ba0f1be fix: skip duplicate suppression for registry rows 2026-04-28 02:59:06 +01:00
Shakker
f049d9dec2 fix: avoid broad model row runtime resolution 2026-04-28 02:59:06 +01:00
Shakker
f5439a341b fix: skip broad provider runtime catalog listing 2026-04-28 02:59:06 +01:00
Shakker
9df9bbd243 refactor: support broad static catalog reads 2026-04-28 02:59:06 +01:00
Peter Steinberger
f64e4fd8cf test: split agents vitest shards 2026-04-28 02:58:24 +01:00
Peter Steinberger
fe1c7fae99 test: catch transitive gateway cold imports 2026-04-28 02:58:06 +01:00
Peter Steinberger
8b6d960539 test: move hot runtime tests to fast lane 2026-04-28 02:57:47 +01:00
Neerav Makwana
ebfc36ba8d docs(changelog): update memory fix attribution 2026-04-28 02:56:56 +01:00
Neerav Makwana
1106cc7fd2 fix(cli): skip memory eager context warmup 2026-04-28 02:56:56 +01:00
Peter Steinberger
1945389374 test: expose provider media test helpers 2026-04-28 02:52:30 +01:00
Peter Steinberger
7f3dead335 perf: keep gateway cold paths out of startup 2026-04-28 02:50:32 +01:00
Peter Steinberger
2746e2ccef test(telegram): cover handler error boundary 2026-04-28 02:50:03 +01:00
Peter Steinberger
2a3a24ebdc refactor: share media provider asset helpers (#73142)
* refactor: share openai-compatible speech providers

* refactor: tighten openai-compatible speech helper

* refactor: share image generation asset helpers

* fix: keep image helpers off root plugin sdk runtime
2026-04-28 02:44:18 +01:00
Peter Steinberger
4949f23219 docs(changelog): clarify parent CLI memory fix 2026-04-28 02:40:44 +01:00
hclsys
ba80695bba fix(cli): exit 0 when invoking parent commands without a subcommand (#73077)
Several `openclaw <parent>` commands (channels, plugins, approvals, devices,
cron, mcp) were exiting with code 1 when invoked bare, while printing the
same help-style content that `<parent> --help` produces (which exits 0).
This broke `&&` chains and surfaced a misleading
`ELIFECYCLE Command failed with exit code 1.` line under pnpm.

Add a small `applyParentDefaultHelpAction(cmd)` helper in
`src/cli/program/parent-default-help.ts` that attaches a default action
which prints the parent's own help and sets `process.exitCode = 0`. The
helper is a no-op when the parent already has its own action (e.g.
`agents` defaulting to `agents list`), so existing intentional defaults
are preserved.

Apply it to the six core parents listed in #73077.
2026-04-28 02:40:44 +01:00
Peter Steinberger
482c74b724 refactor: remove narrow SwiftLint suppressions 2026-04-28 02:38:44 +01:00
Peter Steinberger
152b9856eb test(ci): update support boundary expectations 2026-04-28 02:38:31 +01:00
Peter Steinberger
2d0cc1ee22 fix(memory): reject empty lancedb embedding config 2026-04-28 02:38:31 +01:00
Peter Steinberger
f8a15a06f2 test(models): drop suppression helper exports 2026-04-28 02:38:31 +01:00
Peter Steinberger
947aae5a99 refactor(models): move suppressions to manifests 2026-04-28 02:38:31 +01:00
Peter Steinberger
c0fdf9923b perf(agents): keep model resolution caches warm 2026-04-28 02:38:31 +01:00
Jochen Roessner
e9be25b554 perf: cache model resolution to avoid repeated plugin-provider loads
On ARM64 devices (e.g. Raspberry Pi 4), resolvePluginProviders takes ~20s
on first call. Three bugs cause this cost to be paid repeatedly:

1. ensureOpenClawModelsJson readyCache fingerprint includes models.json
   mtime. After a write, the stored fingerprint (pre-write mtime) never
   matches again, forcing every caller to re-run planOpenClawModelsJson.

2. readyCache has one entry per file path. Agents with different configs
   (e.g. main agent vs active-memory subagent) overwrite each other's
   entry, so neither benefits from caching.

3. resolveExplicitModelWithRegistry calls shouldSuppressBuiltInModel →
   resolveProviderPluginsForCatalogHooks on every agent run. The internal
   cache key includes the full config, so callers with slightly different
   configs each pay the full provider-load cost.

Fixes:
- Remove modelsFileMtimeMs from fingerprint (bug 1)
- Add noopCache to MODELS_JSON_STATE keyed by (path, mtime) — a noop
  result is config-agnostic, so any caller can reuse it (bug 2)
- Cache resolveExplicitModelWithRegistry by (provider, modelId, agentDir),
  stable for the lifetime of a gateway session (bug 3)

Measured on Raspberry Pi 4 (ARM64):
  active-memory subagent preprocessing: 66-75s → ~3s (warm)
  active-memory total elapsed:           ~96s  → ~14s (warm)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 02:38:31 +01:00
Peter Steinberger
a0c850d188 test: stabilize gateway restart loop signals 2026-04-28 02:37:24 +01:00
Peter Steinberger
3efb444002 fix(discord): skip disabled reaction fetches 2026-04-28 02:36:48 +01:00
Peter Steinberger
7d4da9c610 fix(plugins): ignore inherited npm dry-run for runtime deps 2026-04-28 02:36:48 +01:00
Peter Steinberger
13ff3142bd fix(agents): classify terminal results for fallback 2026-04-28 02:35:51 +01:00
Peter Steinberger
82ca94fdd7 test: curate google live profile signal 2026-04-28 02:32:44 +01:00
Peter Steinberger
43a73d6a31 refactor: separate bundled channel schema surface 2026-04-28 02:31:21 +01:00
Peter Steinberger
1cea04ba0f fix(ci): mock gateway run path helpers 2026-04-28 02:30:39 +01:00
Peter Steinberger
de76ad506c test: stabilize release live e2e lanes 2026-04-28 02:30:36 +01:00
Shakker
d9411f9dc1 perf: reuse startup plugin manifests 2026-04-28 02:29:08 +01:00
Shakker
2aacc4053b refactor: accept supplied plugin manifest registry 2026-04-28 02:29:08 +01:00
Peter Steinberger
8db21cdcde chore: update app Swift package releases 2026-04-28 02:28:05 +01:00
Peter Steinberger
e651809084 perf: slim gateway startup imports 2026-04-28 02:26:27 +01:00
Peter Steinberger
b6a90188e7 test: trim hot test runtime imports 2026-04-28 02:25:55 +01:00
Peter Steinberger
fe15268e5f fix: degrade plugin-local reload invalidity 2026-04-28 02:25:00 +01:00
Peter Steinberger
06a80fa813 fix(ci): use managed temp dir in channel contracts 2026-04-28 02:20:01 +01:00
Peter Steinberger
b891dbb133 test: curate openrouter live profile signal 2026-04-28 02:17:48 +01:00
Peter Steinberger
837c4c5f1b fix: respect external channel owners in doctor blockers 2026-04-28 02:15:43 +01:00
Peter Steinberger
6a338ba67d test(cli): align run-main primary registration expectations 2026-04-28 02:14:27 +01:00
Peter Steinberger
d35ada2f54 refactor: relocate channel contract helpers 2026-04-28 02:14:08 +01:00
Peter Steinberger
a66605bf23 fix(cron): skip isolated runs when local providers are down 2026-04-28 02:12:19 +01:00
Peter Steinberger
4e63f710f1 fix(ci): restore plugin install and tooling checks 2026-04-28 02:09:28 +01:00
Peter Steinberger
7c79f0ac9c fix(telegram): centralize update offset tracking 2026-04-28 02:08:22 +01:00
Peter Steinberger
955f0a692a perf: fast-path gateway foreground startup 2026-04-28 02:07:01 +01:00
Peter Steinberger
6b7886e024 test: refresh memory install config fixture 2026-04-28 02:04:24 +01:00
Peter Steinberger
dc4512ad0c refactor: split channel target test helpers 2026-04-28 02:03:15 +01:00
Peter Steinberger
6c859d8c82 fix(memory-lancedb): use neutral memory host import 2026-04-28 01:59:41 +01:00
Peter Steinberger
53906fd177 test: update run-main env mock 2026-04-28 01:59:25 +01:00
Peter Steinberger
a9bd8bb9b4 fix(gateway): surface clean channel exits 2026-04-28 01:59:10 +01:00
Peter Steinberger
53d213f9cc perf: lazy load hot test imports 2026-04-28 01:57:22 +01:00
Peter Steinberger
f5a48efac5 fix(status): report custom memory plugin status 2026-04-28 01:51:37 +01:00
Peter Steinberger
37ea03dbac fix(memory-lancedb): use scoped config runtime import 2026-04-28 01:50:09 +01:00
Peter Steinberger
75e126ef6a perf: improve gateway startup diagnostics 2026-04-28 01:48:00 +01:00
Peter Steinberger
13d3777cf3 fix(plugins): keep config schema on manifest metadata 2026-04-28 01:47:16 +01:00
Peter Steinberger
45a84b5f95 refactor: expose channel contract test helpers 2026-04-28 01:45:58 +01:00
Peter Steinberger
8d9a2f82a4 fix(gateway): keep bundled channel startup light 2026-04-28 01:44:40 +01:00
Peter Steinberger
983fd775e2 fix(memory-core): stream embedding cache seed during reindex
- stream safe-reindex embedding-cache seeding with SQLite iterate()
- avoid no-op empty-cache transactions and keep regression coverage explicit
- supersedes #73067

Thanks @parkertoddbrooks.
2026-04-28 01:44:03 +01:00
Peter Steinberger
2057713af5 fix(memory): let lancedb use provider embedding auth 2026-04-28 01:42:43 +01:00
Peter Steinberger
b294f7c467 fix: harden ios app build hygiene 2026-04-28 01:42:10 +01:00
Peter Steinberger
2fe213ebf2 perf: avoid global config loads in approval tests 2026-04-28 01:41:16 +01:00
Peter Steinberger
4cc42a1d69 fix: reuse plugin metadata for config schemas 2026-04-28 01:37:38 +01:00
Vincent Koc
d93e6f6158 fix(feishu): repair WebSocket reconnect and heartbeat config (#72411) 2026-04-27 17:32:36 -07:00
Peter Steinberger
fdd2ff02c6 ci: stabilize release validation lanes 2026-04-28 01:31:00 +01:00
Peter Steinberger
6ebe3087fc test: narrow live gateway profile signal 2026-04-28 01:30:59 +01:00
TinyClaw
fb5b46ae48 fix(bonjour): suppress ciao crash when networkInterfaces() is denied
Classify ciao interface-enumeration SystemErrors from restricted sandboxes and suppress mDNS advertising instead of letting the Gateway crash.
2026-04-27 17:30:43 -07:00
Peter Steinberger
c72f8f357b fix: harden mac app computer use docs 2026-04-28 01:25:31 +01:00
Peter Steinberger
864c4f7ff4 fix(memory-core): bound fallback vector chunk scoring
- stream fallback Memory Core vector scoring with SQLite iterate() and a bounded top-K result set
- add regression coverage and live-main lint/boundary helper repairs
- supersedes #73069

Thanks @parkertoddbrooks.
2026-04-28 01:23:40 +01:00
Peter Steinberger
56875c4d32 refactor: split generic plugin test fixtures 2026-04-28 01:21:39 +01:00
Peter Steinberger
e508d81f79 perf: avoid registry loads in hot tests 2026-04-28 01:20:47 +01:00
Peter Steinberger
6b1089ffe5 fix: keep group silence on no-reply path 2026-04-28 01:20:00 +01:00
Peter Steinberger
4d4c7c8ab3 fix(plugins): time out hanging agent end hooks 2026-04-28 01:18:50 +01:00
Peter Steinberger
067888a608 fix: surface npm plugin install errors 2026-04-28 01:18:02 +01:00
Peter Steinberger
f34b41f198 refactor: split plugin sdk test helpers 2026-04-28 01:14:19 +01:00
Vincent Koc
d88610cf2b test: avoid bundled extension boundary false positive 2026-04-27 17:13:21 -07:00
Peter Steinberger
48a0be8ff3 docs(plugins): document channel route sdk 2026-04-28 01:13:01 +01:00
Peter Steinberger
e27c32b9b0 refactor(plugin-sdk): publish route helpers 2026-04-28 01:13:01 +01:00
Peter Steinberger
f368d3b49f refactor(channels): share route identity keys 2026-04-28 01:13:00 +01:00
Peter Steinberger
3eec9e4642 refactor(channels): reuse route context helpers 2026-04-28 01:13:00 +01:00
Peter Steinberger
3876682635 refactor(channels): centralize route normalization 2026-04-28 01:13:00 +01:00
Peter Steinberger
0294aebe6f feat(providers): add DeepInfra provider plugin (#73038)
* feat(providers): add DeepInfra provider plugin

* feat(deepinfra): add media provider surfaces

* fix(deepinfra): satisfy provider boundary checks

* docs: add gitcrawl maintainer skill

* test: include deepinfra in live media sweeps

* fix: remove stale tts contract import
2026-04-28 01:12:54 +01:00
Peter Steinberger
1fde7dbc0e fix(memory): support embedding providers without encoding format 2026-04-28 01:12:34 +01:00
Peter Steinberger
100c595fbc test: fix host hook contract helper import 2026-04-28 01:11:56 +01:00
Peter Steinberger
ae7f365fbc fix: stop native approval auth retry loops 2026-04-28 01:10:04 +01:00
EVA
1adaa28dc8 [plugin sdk] Add generic plugin host-hook contracts (#72287)
Merged via squash.

Prepared head SHA: 68e5f2ce19
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-27 17:07:02 -07:00
Peter Steinberger
ef1e97472f fix: remove stale tts contract import 2026-04-28 01:05:54 +01:00
Peter Steinberger
1d3170b16f fix: scope skills cli to active agent workspace 2026-04-28 01:05:36 +01:00
Peter Steinberger
32d76e2429 fix(memory): bound lancedb recall embedding queries 2026-04-28 00:58:30 +01:00
Peter Steinberger
47f40788cf ci: install ffmpeg for live audio media shard 2026-04-28 00:57:43 +01:00
Peter Steinberger
7f77ecff77 chore: refresh plugin sdk api baseline 2026-04-28 00:55:11 +01:00
Peter Steinberger
8057561cee refactor: promote plugin test helpers to sdk 2026-04-28 00:55:11 +01:00
Peter Steinberger
49d069cd94 test: distinguish release live shard partitions 2026-04-28 00:54:28 +01:00
Peter Steinberger
7aeb7c2a14 perf: speed up reset model tests 2026-04-28 00:52:07 +01:00
Peter Steinberger
da3cf1c1a8 fix: preserve bundled facade fallback semantics 2026-04-28 00:50:34 +01:00
Peter Steinberger
b90f29d313 ci: split native live release shards 2026-04-28 00:49:10 +01:00
Peter Steinberger
3f94f25a3c test(plugins): parse boundary import syntax 2026-04-28 00:46:15 +01:00
pashpashpash
a412603bad fix(codex): honor effective stdio env for fallback auth 2026-04-28 00:46:15 +01:00
pashpashpash
401ae38f13 fix(codex): keep env fallback local to stdio app-server 2026-04-28 00:46:15 +01:00
Peter Steinberger
5f15bea6ce fix(codex): bootstrap app-server auth fallback 2026-04-28 00:46:15 +01:00
pashpashpash
a1c88f3ebe fix(codex): hash app-server env values in client keys 2026-04-28 00:46:15 +01:00
pashpashpash
20ff49f7c8 fix(codex): auto-clear api key for subscription auth 2026-04-28 00:46:15 +01:00
pashpashpash
aeb007e4e5 fix(codex): expose app-server env controls 2026-04-28 00:46:15 +01:00
Peter Steinberger
09c39463bb test: tolerate xAI realtime STT brand spelling 2026-04-28 00:45:14 +01:00
Peter Steinberger
f3d53ce22c fix: clarify memory embedding concurrency help 2026-04-28 00:39:18 +01:00
Peter Steinberger
697d85aefe fix: auto-register bundled computer use marketplace 2026-04-28 00:36:19 +01:00
Peter Steinberger
802f13ac15 fix(memory): cap ollama non-batch embedding concurrency 2026-04-28 00:34:18 +01:00
Peter Steinberger
5de3196a60 test: satisfy plugin contract boundaries 2026-04-28 00:33:46 +01:00
Peter Steinberger
0aef33f6c4 perf: reduce persistent dedupe test disk work 2026-04-28 00:31:06 +01:00
Peter Steinberger
fc055e2393 fix: speed up Telegram status diagnostics 2026-04-28 00:28:22 +01:00
Peter Steinberger
3ae796b649 test: keep SDK testing off bundled inventory 2026-04-28 00:28:09 +01:00
Peter Steinberger
0a0d934725 test: relax OpenAI live transcription assertion 2026-04-28 00:27:37 +01:00
Peter Steinberger
90b6665ded refactor: move plugin api test helper to sdk 2026-04-28 00:24:54 +01:00
Peter Steinberger
f71f5bc586 fix: repair packaged plugin runtime mirrors 2026-04-28 00:23:38 +01:00
Peter Steinberger
152e30935f fix: use public provider test helpers in live image test 2026-04-28 00:17:29 +01:00
Peter Steinberger
56ef6334f0 perf: combine pty exec coverage 2026-04-28 00:17:03 +01:00
Peter Steinberger
62f8cff33a fix: avoid full runtime dependency restaging 2026-04-28 00:15:15 +01:00
Peter Steinberger
d462d1faf2 refactor: move plugin contracts onto SDK testing seams 2026-04-28 00:14:58 +01:00
Peter Steinberger
d3e4640bed fix(acpx): ignore Codex ACP timeout config 2026-04-28 00:12:34 +01:00
Peter Steinberger
d74c8423c7 test: fix plugin runtime env test types 2026-04-28 00:11:47 +01:00
Peter Steinberger
1776840c57 fix: preserve typed runtime env casts 2026-04-28 00:08:32 +01:00
Peter Steinberger
05a93c1788 perf: avoid sdk client setup in openai transport test 2026-04-28 00:07:29 +01:00
Peter Steinberger
2fbbc6e2fa docs: clarify plugin disable doctor behavior 2026-04-28 00:07:02 +01:00
Peter Steinberger
f1edd601bc ci: split release qa parity lanes 2026-04-28 00:05:33 +01:00
Peter Steinberger
ff2b2e769f fix(cron): preserve job model fallbacks 2026-04-28 00:03:01 +01:00
Peter Steinberger
da6d8940a0 refactor: clean runtime env helper types 2026-04-28 00:02:24 +01:00
Peter Steinberger
ccc9dd5eef fix: keep session history redaction forced 2026-04-27 23:59:47 +01:00
Peter Steinberger
5e8cc1d9c2 docs: add changelog for plugin disable startup fast path (#73041) 2026-04-27 23:57:31 +01:00
Intern Dev
f07844450c Prevent disabled plugins from warming the gateway plugin graph
A local containment profile uses plugins.enabled=false to stop plugin and channel runtime churn. The previous startup path still built plugin lookup tables and doctor stale scans despite the global disable, which made the switch noisy and slow.

Constraint: plugins.enabled=false must leave channel blocker warnings intact while treating stale plugin config as inert.
Rejected: Clear user plugin config automatically | would mutate a reversible containment setting.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce plugin registry discovery before checking plugins.enabled.
Tested: pnpm test src/gateway/server-startup-plugins.test.ts src/config/plugin-auto-enable.core.test.ts src/commands/doctor/shared/stale-plugin-config.test.ts src/commands/doctor/shared/preview-warnings.test.ts
Tested: pnpm check:changed
Tested: pnpm build
2026-04-27 23:57:31 +01:00
Peter Steinberger
5bdfc251ff test(plugins): assert runtime mirror reload stability 2026-04-27 23:57:12 +01:00
Peter Steinberger
c27b82d431 perf: avoid heavy imports in hot tests 2026-04-27 23:57:00 +01:00
Peter Steinberger
39a2d1da96 docs(codex): add computer use guide 2026-04-27 23:56:25 +01:00
Peter Steinberger
78d3fce5f9 fix: preserve OpenAI encrypted reasoning replay 2026-04-27 23:54:16 +01:00
Peter Steinberger
ea2d95e23e refactor(codex): clarify computer use setup state 2026-04-27 23:53:53 +01:00
Peter Steinberger
87345c0667 fix: narrow bundled runtime mirror materialization 2026-04-27 23:52:52 +01:00
Peter Steinberger
9f9bcfe231 perf: reduce hot test imports and duplicate scans 2026-04-27 23:47:26 +01:00
Peter Steinberger
f7815cdd8f fix(codex): harden computer use setup states 2026-04-27 23:46:16 +01:00
Peter Steinberger
f7983a07a4 refactor: move plugin runtime env helper 2026-04-27 23:45:26 +01:00
Peter Steinberger
0df6e5a473 refactor: expose plugin test helpers via sdk 2026-04-27 23:45:26 +01:00
Peter Steinberger
6f09039b0c fix(plugins): reuse unchanged runtime mirrors 2026-04-27 23:45:02 +01:00
JK
323030594e fix(agents): resolve model aliases in sessions_spawn (#59681)
* fix(agents): resolve model aliases in sessions_spawn

normalizeModelSelection() only trims the input — it never resolves
aliases through the model alias index. When a user passes an alias
like 'opus' to sessions_spawn, the child session gets patched with
the raw string, which the gateway cannot match to any provider.

Add resolveModelThroughAliases() to check bare strings against the
configured alias map before returning from
resolveSubagentSpawnModelSelection().

Fixes #57532
Refs #50736

* refactor: address review feedback on alias resolution

- Accept pre-built ModelAliasIndex instead of rebuilding per call
- Narrow helper signature to (string, ModelAliasIndex) → string
- Remove unreachable ?? raw fallback

Co-Authored-By: greptile-apps[bot]

* fix(agents): resolve sessions_spawn model aliases

---------

Co-authored-by: HowdyDooToYou <HowdyDooToYou@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-04-27 15:44:56 -07:00
Olamiposi
c51e315f3a docs: clarify messaging vs full tool profiles (#39954)
* docs: clarify messaging vs full tool profiles

* docs: normalize tools.profile references

* docs: clarify messaging and full tool profiles

---------

Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-04-27 15:44:17 -07:00
Vincent Koc
cc80a40d86 fix(ci): preserve mixed macOS CodeQL SARIF findings
Conservatively filter macOS CodeQL SARIF by dropping only findings where every location is SwiftPM build output. Verified with workflow sanity, local jq filtering, PR CI, and a failed-job rerun for an unrelated stalled Vitest shard.
2026-04-27 15:43:53 -07:00
neilofneils404
482ff924ef fix: pass directories to provider stream wrappers (#67843)
* fix: pass directories to provider stream wrappers

* fix: pass directories to provider stream wrappers

---------

Co-authored-by: neilofneils404 <258699186+neilofneils404@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-04-27 15:43:38 -07:00
Vincent Koc
94f5827c6e docs(logging): note Control UI tool payload redaction
Document the redaction surface added in f3e8c50df3: custom logging.redactPatterns now apply to Control UI tool start args, partial/final result payloads, derived exec output, and patch summaries on top of the built-in defaults.
2026-04-27 15:39:49 -07:00
Peter Steinberger
39e3d8d31d ci: shard release validation reruns 2026-04-27 23:38:13 +01:00
Peter Steinberger
d2320e4d4b fix(models): keep user model switches strict 2026-04-27 23:32:44 +01:00
Peter Steinberger
496a5eb56f fix: dedupe silent reply prompt guidance 2026-04-27 23:31:13 +01:00
Peter Steinberger
ccfa0c1964 docs: clarify hook config and feishu policy 2026-04-27 23:30:57 +01:00
volcano303
f3e8c50df3 fix(agents): redact Control UI tool payload secrets (#72319)
Fixes #72283.

- Redacts Control UI tool start args, partial/final result payloads, derived exec output, and patch summaries before event emission.
- Forces tool/UI payload redaction to include built-in patterns plus configured custom `logging.redactPatterns`.
- Covers object, details-only, primitive string, and top-level array tool-result shapes.

Tests:
- `pnpm test src/agents/pi-embedded-subscribe.tools.test.ts src/agents/pi-embedded-subscribe.handlers.tools.test.ts`
- `pnpm check:changed`

Co-authored-by: volcano303 <75143900+volcano303@users.noreply.github.com>
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
2026-04-27 23:30:50 +01:00
Vincent Koc
24c39de9c1 test(memory): allow packed index suite timeout
Allow the memory index suite to exceed the global 120s test timeout when it runs inside a packed extension shard. The scoped Vitest config is reset after the file.
2026-04-27 15:30:47 -07:00
Peter Steinberger
dd0f5937d2 fix(doctor): avoid companion gateway service false positives 2026-04-27 23:30:29 +01:00
Peter Steinberger
36d3722a96 fix(cli): disable source checkout compile cache 2026-04-27 23:28:17 +01:00
Vincent Koc
6e77c10c6c fix(ci): harden macOS CodeQL SARIF filtering
Harden the macOS CodeQL SARIF filter to drop only findings whose primary location is SwiftPM build output. Verified with workflow sanity, local jq filtering, full PR CI, and profile=macos-security branch proof in 18m44s.
2026-04-27 15:25:38 -07:00
Peter Steinberger
0cc3c027a8 test: avoid slow home lookups in service audit tests 2026-04-27 23:23:15 +01:00
Peter Steinberger
48e91f09d5 fix(cli): fail empty local model probes 2026-04-27 23:16:39 +01:00
Peter Steinberger
81390c643b fix(update): restart Windows startup gateway after update 2026-04-27 23:16:20 +01:00
Peter Steinberger
abf5dea7dd fix(daemon): filter missing service path fallbacks 2026-04-27 23:16:04 +01:00
Peter Steinberger
bf4306d1b0 refactor: route plugin test helpers through sdk 2026-04-27 23:12:21 +01:00
Peter Steinberger
7975305a89 test: cover trusted-proxy secret surfaces 2026-04-27 23:10:22 +01:00
Peter Steinberger
1a98938479 fix: allow trusted-proxy local password fallback 2026-04-27 23:10:22 +01:00
Vincent Koc
61a18e5596 fix(agent): preserve default-agent session routing compatibility (#72414)
* fix(agent): preserve default-agent session routing compatibility

* fix(clownfish): address review for ghcrawl-207038-agentic-merge (1)

* fix(agent): migrate legacy default-agent sessions

* fix(slack): use narrow agent runtime import
2026-04-27 15:09:01 -07:00
Peter Steinberger
5488175b22 test: give xai live search more headroom 2026-04-27 23:07:52 +01:00
Peter Steinberger
42dddbbe78 fix(cli): streamline local model probes 2026-04-27 23:02:26 +01:00
Peter Steinberger
d7dcd0e21e test: stabilize release validation lanes 2026-04-27 23:00:45 +01:00
Peter Steinberger
6f80ba7b78 fix(test): avoid memory provider discovery in registration test 2026-04-27 22:58:55 +01:00
Peter Steinberger
8599fdda4a test: keep extension mocks on sdk seams 2026-04-27 22:55:09 +01:00
Peter Steinberger
c35a96bcbc fix(test): use focused plugin sdk test seams 2026-04-27 22:47:57 +01:00
Peter Steinberger
24b45a038c fix(gateway): bound supervised lock recovery 2026-04-27 22:44:37 +01:00
Peter Steinberger
43ababf96b fix(gateway): keep startup sidecars responsive 2026-04-27 22:44:37 +01:00
Peter Steinberger
75c03b28e0 test(memory): reset timers in index suite 2026-04-27 22:41:56 +01:00
Peter Steinberger
d519dc6976 docs(channels): add channel docking concept 2026-04-27 22:37:58 +01:00
Vincent Koc
2c2a240344 fix(ci): filter macOS CodeQL dependency SARIF
Filter SwiftPM dependency build results from the manual macOS CodeQL shard before upload. Verified with workflow sanity, local jq filtering, and profile=macos-security branch proof in 15m54s. PR CI has the same unrelated extensions/memory-core timeout failure currently present on main.
2026-04-27 14:37:29 -07:00
Peter Steinberger
7807e8118c perf(test): slim codex web search test imports 2026-04-27 22:34:48 +01:00
Peter Steinberger
a8c548f4f3 test: route extension tests through sdk seams 2026-04-27 22:34:21 +01:00
Peter Steinberger
46ba8e7cce feat(plugin-sdk): expose extension test seams 2026-04-27 22:34:21 +01:00
Peter Steinberger
582debbec8 docs(channels): explain dock commands 2026-04-27 22:32:44 +01:00
Peter Steinberger
d24b78e96d test(extensions): use scoped config runtime imports 2026-04-27 22:24:30 +01:00
Peter Steinberger
2216ce3018 test: use narrow config sdk imports 2026-04-27 22:22:34 +01:00
Omar Shahine
da3d17e1ca fix(tts): pre-transcode synthesized audio to opus-in-CAF for native iMessage voice-memo bubbles via BlueBubbles (#72586)
End-to-end testing on macOS + BlueBubbles + ElevenLabs walked through three CAF flavors before landing on the format Apple's Messages.app actually emits when a user records a native iMessage voice memo:

- PCM int16 @ 44.1 kHz CAF: BlueBubbles' internal `afconvert -f m4af -d aac` conversion fails; the original CAF reaches iMessage but renders with 0 s duration.
- AAC @ 22.05 kHz mono CAF: BlueBubbles' conversion succeeds and the server silently downgrades the delivery, sending the converted MP3 as a generic audio attachment.
- **Opus @ 24 kHz mono CAF**: byte-identical to the descriptor block Apple's Messages.app produces; BlueBubbles passes it through unchanged and iMessage renders a native voice-memo bubble with proper duration and waveform UI.

Adds an opt-in `tts.voice.preferAudioFileFormat` channel capability and a macOS `afconvert`-backed pre-transcode in the speech-core pipeline. BlueBubbles declares `preferAudioFileFormat: "caf"`. Other channels are unaffected. Falls back to the original buffer when the host platform, the source/target pair, or the transcoder process can't produce the preferred container — so non-Darwin hosts and unsupported provider combinations are unchanged.

Also adds a `caff` magic-byte sniff in `src/media/mime.ts` so the auto-reply host-local-media validator (which uses `file-type` and didn't recognize CAF natively) accepts the buffer instead of dropping it as "⚠️ Media failed."

Fixes #72506.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:15:16 -07:00
Peter Steinberger
fb4d9fc4fb ci: harden npm telegram artifact upload 2026-04-27 22:13:21 +01:00
Peter Steinberger
295d63c331 ci: record package proof in release evidence 2026-04-27 22:00:03 +01:00
Peter Steinberger
1eea534ddb fix(channels): handle generated dock commands 2026-04-27 21:59:15 +01:00
Peter Steinberger
74e62c32c3 test: route extension tests through sdk subpaths 2026-04-27 21:58:48 +01:00
Peter Steinberger
662de55e07 refactor: expose extension sdk boundary seams 2026-04-27 21:58:48 +01:00
Peter Steinberger
3e497f5e2b fix: accept cron delivery thread ids 2026-04-27 21:56:47 +01:00
Peter Steinberger
18ef83c0da fix(feishu): preserve disabled group policy for explicit groups 2026-04-27 21:55:33 +01:00
Peter Steinberger
b3bc60ae25 fix(msteams): unwrap jwt runtime deps 2026-04-27 21:53:51 +01:00
Vincent Koc
bd51f82efa fix(security): harden CodeQL secret ref validation
Remediate current-profile CodeQL findings for file SecretRef id validation and release workflow job permissions. Includes changelog credit. Thanks @vincentkoc.
2026-04-27 13:53:27 -07:00
Peter Steinberger
f2ba8ca927 test: stabilize bundled channel docker smoke 2026-04-27 21:48:26 +01:00
Peter Steinberger
1787d3be07 fix(gateway): scope startup provider discovery 2026-04-27 21:45:54 +01:00
Peter Steinberger
28d9fc5f20 fix(test): include cron delivery thread id in schema keys 2026-04-27 21:43:12 +01:00
Peter Steinberger
db622c67d1 perf(test): slim directive and run-param imports 2026-04-27 21:42:31 +01:00
Vincent Koc
36b5e34fc0 fix(ci): add macOS CodeQL security shard
Add a manual macOS CodeQL security shard scoped to app sources. Verified with profile=macos-security on Blacksmith in 16m55s.
2026-04-27 13:40:34 -07:00
Peter Steinberger
b6be422306 fix(cron): accept threaded delivery in gateway schema 2026-04-27 21:37:18 +01:00
Peter Steinberger
599b1b8462 fix(cloudflare-ai-gateway): strip anthropic thinking prefill 2026-04-27 21:36:50 +01:00
Vincent Koc
013939cfc7 fix(gateway): preserve repeated characters in chat stream merge (#72400)
* fix(gateway): preserve repeated characters in chat stream merge

* fix(gateway): cap live chat stream buffers
2026-04-27 13:35:57 -07:00
Peter Steinberger
59faa023fe fix(gateway): unblock sidecar startup 2026-04-27 21:34:44 +01:00
Peter Steinberger
e60905d754 fix: harden bonjour DNS label truncation (#73022) 2026-04-27 21:33:02 +01:00
Peter Steinberger
7d2d8af3ab fix(plugins): fast-path strict manifest json 2026-04-27 21:27:02 +01:00
Peter Steinberger
11e6928b3e fix: keep runtime context out of user turns 2026-04-27 21:24:56 +01:00
Gustavo Madeira Santana
b9fd13e8d7 qa-matrix: add streaming tool progress scenarios 2026-04-27 16:21:37 -04:00
Gustavo Madeira Santana
3132f4990c qa-lab: generalize tool progress prompts 2026-04-27 16:21:36 -04:00
Gustavo Madeira Santana
24068f19c6 matrix: stream tool progress in previews 2026-04-27 16:21:34 -04:00
MoerAI
01e153986a fix(feishu): admit groups explicitly listed under channels.feishu.groups (#67687)
Feishu config defaults groupPolicy to 'allowlist'. Inbound group handling read groupAllowFrom and called isFeishuGroupAllowed before resolveFeishuReplyPolicy was reached, so a config that only set channels.feishu.groups.<chat_id>.requireMention=false (with no groupAllowFrom) was rejected with 'group not in groupAllowFrom' before per-group requireMention could take effect. Treat the explicit presence of a group entry under channels.feishu.groups as the operator's allowlist signal: if groupConfig is defined, skip the empty-allowlist rejection. resolveFeishuReplyPolicy still owns mention gating, and existing groupConfig.enabled=false / groupAllowFrom-driven rejections are preserved. Adds a regression test that exercises the reporter's exact config shape and confirms inbound text reaches finalize/dispatch.
2026-04-27 21:19:49 +01:00
Peter Steinberger
346d5c28c1 test(acp): use typed attachment root fixture 2026-04-27 21:19:45 +01:00
Peter Steinberger
8cc06fff2c test(acp): cover media agent dir dispatch 2026-04-27 21:19:45 +01:00
luyao618
2b578c3a9e fix(agents): pass agentDir to media understanding in ACP dispatch path
The ACP dispatch path calls applyMediaUnderstanding without the agentDir
parameter. This prevents the media understanding pipeline from locating
agent-specific models.json and auth profiles, causing image understanding
to fail silently for non-visual models configured with a separate image
understanding model.

The non-ACP reply path (get-reply.ts) already passes agentDir correctly.
This aligns the ACP path with the same behavior.

Closes #55046

AI-assisted (built with Hermes orchestration).
2026-04-27 21:19:45 +01:00
Peter Steinberger
be2196c6cb test(plugins): cover hook plugin config context 2026-04-27 21:19:41 +01:00
Ayumi Server
c1187109c8 fix: shallow-copy event to avoid mutating shared hook object
Address review feedback on PR #72888. triggerInternalHook passes the
same event reference to all handlers sequentially. Mutating evt.context
leaks pluginConfig to subsequent handlers and causes cross-plugin
overwrites. Shallow-copy event and context instead.
2026-04-27 21:19:41 +01:00
Ayumi Server
ed0b098d75 fix: inject pluginConfig into hook handler event context
When plugins register hooks via api.registerHook(), pluginConfig from
openclaw.json was not available in the hook event context. Plugins that
accessed ctx.pluginConfig or event.context.pluginConfig received
undefined, causing silent failures or fallback to defaults.

Changes:
- Add pluginConfig parameter to registerHook() function
- Wrap handler to inject pluginConfig into event.context before invocation
- Pass params.pluginConfig through createApi() call site

Fixes #72880
2026-04-27 21:19:41 +01:00
RayWoo
ad6e1cd3a0 fix(memory-core): raise NARRATIVE_TIMEOUT_MS from 15s to 60s
Closes #72837. The 15s narrative-subagent timeout was empirically too
tight for warm-gateway runs across light, REM, and deep phases —
gpt-5.4-mini latency through OpenAI alone routinely brushes 12s+, so the
first sweep after a restart deterministically times out across all three
phases. 60s gives realistic LLM-call headroom while still capping the
worst case at one minute, preserving the original comment's "don't leave
parent cron running for minutes" constraint.

Test: updates the matching toMatchObject assertion in
dreaming-narrative.test.ts from 15_000 to 60_000.
2026-04-27 21:19:38 +01:00
Vincent Koc
16322d5cfc fix(bonjour): harden DNS label truncation 2026-04-27 21:19:26 +01:00
luyao618
9ac0b7edbc fix(bonjour): truncate mDNS service name and hostname to 63-byte DNS label limit
When the system hostname exceeds 63 bytes (common with Kubernetes pod
names), the @homebridge/ciao DNS label encoder throws an AssertionError
that crashes the gateway on startup.

Add truncateToDnsLabel() that safely truncates UTF-8 strings at byte
boundaries, applied to both the service instance name and hostname
before passing them to ciao.

Closes #37705

AI-assisted (built with Hermes orchestration).
2026-04-27 21:19:26 +01:00
ryuhaneul
f5b01c1e0e fix(docker): install ca-certificates in slim runtime base
Commit 2cd23957c0 ("build: use slim docker runtime") switched the
runtime image from `node:24-bookworm` (full) to `node:24-bookworm-slim`.
The slim base does not ship `ca-certificates`, and the runtime stage's
`apt-get install` line was not updated to add it.

Result on the resulting image:
- `/etc/ssl/certs/` is empty (`ls /etc/ssl/certs/ | wc -l` == 0)
- `dpkg -l ca-certificates` reports `un` (not installed)
- `update-ca-certificates` is missing in `$PATH` (exit 127)
- every HTTPS outbound from the gateway dies at TLS handshake with
  `error setting certificate file: /etc/ssl/certs/ca-certificates.crt`
- channel plugins that use `node fetch` (telegram/discord/slack)
  crash-loop with `Network request for 'deleteWebhook' failed!`
  and pin the gateway main thread at ~100% CPU on retry.

Verified by rebuilding the runtime image with this patch and
confirming inside the container:
- `ls /etc/ssl/certs/ | wc -l` -> 285
- `curl -4 https://api.telegram.org/` -> 302
- `curl -4 https://www.google.com/`   -> 200
- channel plugins (telegram/discord/slack) register cleanly,
  gateway main-thread CPU returns to idle.

Add `ca-certificates` to the apt-install list and call
`update-ca-certificates` to populate the CA bundle.

Signed-off-by: ryuhaneul <luj.moonlight@gmail.com>
2026-04-27 21:19:22 +01:00
iot2edge
98928388db fix(cli): clarify completion cache timeout message after openclaw update
When the post-update completion cache refresh times out (slow disk,
large bundled plugin tree, Docker overlayfs), the user previously saw
the opaque 'Completion cache update failed: Error: spawnSync
/usr/bin/node ETIMEDOUT'. Detect ETIMEDOUT specifically, surface
'timed out after 30s', and append a manual refresh hint pointing at
'openclaw completion --write-state' so users know it's non-fatal and
how to recover.

Fixes #72842
2026-04-27 21:19:18 +01:00
Peter Steinberger
cdf88bcad4 test: harden release qa live gates 2026-04-27 21:16:48 +01:00
Peter Steinberger
71c74b766e fix(plugins): avoid hand-built extension path markers 2026-04-27 21:12:09 +01:00
Peter Steinberger
465b621cf1 fix(sessions): avoid guarded route-only entries 2026-04-27 21:11:12 +01:00
Peter Steinberger
d62cb3c681 docs(changelog): credit pending low-risk fixes 2026-04-27 21:09:20 +01:00
Peter Steinberger
911be12648 docs: credit bare reset transcript fix 2026-04-27 21:08:50 +01:00
Maho Pan
1dbc250b1a fix: keep bare reset transcript prompt non-empty 2026-04-27 21:08:50 +01:00
Peter Steinberger
03bfdbb052 fix: stage mirrored bundled runtime deps 2026-04-27 21:07:40 +01:00
Peter Steinberger
ff52e281aa perf(test): slim responses payload policy imports 2026-04-27 21:06:40 +01:00
Peter Steinberger
08e7561972 ci: broaden extension boundary guards 2026-04-27 21:02:53 +01:00
Peter Steinberger
e9b1fbb8c4 refactor: pin remaining extension api surfaces 2026-04-27 21:02:53 +01:00
Peter Steinberger
221bfc8929 docs: credit media MIME sanitizer fix 2026-04-27 21:02:30 +01:00
volcano303
e7b87217a2 fix(media): anchor sanitizeMimeType regex and reject trailing junk
Add an end anchor to the type/subtype match and explicitly accept the
RFC 9110 ;parameter tail. Inputs like "image/png<script>" or
"application/json garbage" now return undefined instead of silently
matching the leading prefix.

Closes #9795
2026-04-27 21:02:30 +01:00
Peter Steinberger
1f256306c9 test: align gateway tests with config io split 2026-04-27 21:02:26 +01:00
Peter Steinberger
5e49e8590d fix(cli): resolve message channel plugin scopes 2026-04-27 21:02:09 +01:00
Peter Steinberger
0c305596a2 fix(channels): skip route updates without session creation 2026-04-27 21:00:49 +01:00
haishmg
d32903c283 docs(providers): sort provider directory 2026-04-27 21:00:04 +01:00
Peter Steinberger
a2b84e98e9 fix: clean up trajectory sidecars 2026-04-27 20:58:28 +01:00
Peter Steinberger
9402bca614 fix: limit session list enrichment 2026-04-27 20:58:02 +01:00
Peter Steinberger
72f3c840c7 fix(cli): narrow message plugin registry loads 2026-04-27 20:55:56 +01:00
Peter Steinberger
161b722303 test(gateway): mock split config modules 2026-04-27 20:54:23 +01:00
Peter Steinberger
930b443c9e fix(ollama): preserve streaming usage compat 2026-04-27 20:54:22 +01:00
Vincent Koc
cff991c88d fix(ui): stabilize WebChat final reload reconciliation (#72325)
* fix(ui): stabilize WebChat final reload reconciliation

* fix(clownfish): address review for ghcrawl-165991-agentic-merge (1)

* fix(ui): keep plain control-token text visible
2026-04-27 12:52:39 -07:00
Peter Steinberger
f56897259e fix(cli): keep route-first json stdout clean 2026-04-27 20:51:50 +01:00
Peter Steinberger
f0000ab72d refactor(plugin-sdk): split infra runtime barrel 2026-04-27 20:50:35 +01:00
Peter Steinberger
d7c3a77b93 fix(telegram): skip polling webhook probe 2026-04-27 20:49:57 +01:00
Peter Steinberger
5a23032adb fix(plugins): detect install root rebinding 2026-04-27 20:47:54 +01:00
Peter Steinberger
f6b2ba4a10 fix(control-ui): coalesce duplicate chat submits 2026-04-27 20:45:28 +01:00
Peter Steinberger
8cddb6ce7d fix(webchat): drop stale optimistic assistant tails 2026-04-27 20:45:28 +01:00
Peter Steinberger
6dc8bd8935 fix(gateway): read active transcript history branch 2026-04-27 20:45:28 +01:00
Peter Steinberger
9645fe72c6 test: harden release validation live shards 2026-04-27 20:45:25 +01:00
Peter Steinberger
f90972d942 fix: install plugins through symlinked extension roots 2026-04-27 20:42:37 +01:00
Peter Steinberger
a6adc5f4f1 test(gateway): mock runtime config io imports 2026-04-27 20:40:54 +01:00
Peter Steinberger
f7d2b396d6 fix(test): restore gateway fixture startup config 2026-04-27 20:36:32 +01:00
Peter Steinberger
1fc19ffe11 refactor: narrow messaging public api barrels 2026-04-27 20:34:36 +01:00
Peter Steinberger
a20f97f728 refactor: narrow extension runtime api barrels 2026-04-27 20:34:35 +01:00
Peter Steinberger
31e529f000 ci: guard extension wildcard reexports 2026-04-27 20:34:35 +01:00
Peter Steinberger
f7d67b8ea8 fix(channels): ignore persisted auth for auto-enable 2026-04-27 20:33:43 +01:00
Peter Steinberger
dec1f68d7e fix(litellm): honor noninteractive custom base url 2026-04-27 20:33:04 +01:00
Vincent Koc
74eccd42d8 fix(ci): add android CodeQL security shard
Add a manual Android CodeQL security shard scoped to app production sources. Verified with profile=android-security on Blacksmith in 4m22s.
2026-04-27 12:32:55 -07:00
Peter Steinberger
4cd68fafbb fix(sessions): ignore future freshness timestamps 2026-04-27 20:30:59 +01:00
Peter Steinberger
54e13d4910 ci: split release validation slow shards 2026-04-27 20:30:17 +01:00
Peter Steinberger
2f488b7e7a docs: clarify ClawHub plugin discovery 2026-04-27 20:26:49 +01:00
Peter Steinberger
dc76963e36 fix(gateway): bind startup cron hook to live state 2026-04-27 20:25:46 +01:00
Peter Steinberger
7829c438a6 fix(tts): keep final webchat audio supplemental 2026-04-27 20:22:18 +01:00
Peter Steinberger
d2b0ff808a fix(gateway): ignore broken pipe crashes 2026-04-27 20:17:04 +01:00
Vincent Koc
3cb460873d fix(ui): stabilize agent model selection on switch (#72328)
* fix(ui): stabilize agent model selection on switch

* docs(changelog): credit projectclownfish fixes
2026-04-27 12:06:02 -07:00
Val Alexander
b393febbfa chore: remove coven changelog entries
Remove the two Unreleased Coven ACP/runtime changelog bullets that were reintroduced after the Coven extension removal.\n\nVerification:\n- rg -n -i "coven" CHANGELOG.md Swabble/CHANGELOG.md extensions/matrix/CHANGELOG.md apps/ios/CHANGELOG.md\n- git diff --check origin/main..HEAD\n- PR checks passed on head 767c274b0f
2026-04-27 14:05:23 -05:00
dependabot[bot]
48f433479d chore(deps): bump github/codeql-action
Bump github/codeql-action from b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 to 95e58e9a2cdfd71adc6e0353d5c52f41a045d225.
2026-04-27 12:01:27 -07:00
Vincent Koc
282af9c50a fix(ci): run CodeQL on small Blacksmith runners (#72988) 2026-04-27 11:56:48 -07:00
kakahu
d70808433d Add structured Matrix approval metadata (#72432)
Merged via squash.

Prepared head SHA: 0e06533dff
Co-authored-by: kakahu2015 <17962485+kakahu2015@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-27 14:52:02 -04:00
Vincent Koc
d0be08a9a4 fix(github): action manual Barnacle triage labels
Human-applied Barnacle triage candidate labels now trigger the intended auto-response while bot-applied heuristic candidates remain passive.
2026-04-27 11:44:47 -07:00
Vincent Koc
e864fd39cc fix(ci): narrow CodeQL critical scan (#72982) 2026-04-27 11:42:42 -07:00
Vincent Koc
1497425b8d fix(gateway): trim startup config imports 2026-04-27 11:34:24 -07:00
Vincent Koc
acbf57b448 revert(acp): remove Coven bridge
Revert the bundled Coven ACP bridge extension, its ClawHub publishing wiring, and related ACP/proxy runtime changes.
2026-04-27 11:26:05 -07:00
Val Alexander
f7797ca62b chore: remove coven extension 2026-04-27 13:22:32 -05:00
NVIDIAN
dc96886378 fix: clean up bundled LSP process trees on shutdown
Fixes #72357
2026-04-27 11:10:56 -07:00
Vincent Koc
d9bef3fe7c fix(ui): discard stale config state on explicit reload (#72624)
* fix(ui): discard stale config state on explicit reload

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

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

* test(ui): align channel config host state
2026-04-27 11:10:38 -07:00
Vincent Koc
be6263da4f fix(gateway): preserve runtime-backed health state (#72417)
* fix(gateway): preserve runtime-backed health state

* fix(clownfish): address review for ghcrawl-207035-agentic-merge (1)

* fix(gateway): harden health snapshot exposure
2026-04-27 11:04:59 -07:00
Vincent Koc
2161b46032 fix(feishu): support native interactive card payload sends (#72667)
* fix(feishu): support native interactive card payload sends

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

* fix(feishu): harden native card payload rendering
2026-04-27 11:02:15 -07:00
Gustavo Madeira Santana
c5678194d4 docs(qa): document Telegram and Discord QA lanes against code
Both lanes had only one paragraph each in qa-e2e-automation.md. Adds a
"Telegram and Discord QA reference" section verified against
extensions/qa-lab/src/live-transports/{telegram,discord}/* with:

- shared CLI flags table (--scenario, --output-dir, --repo-root, --sut-account,
  --provider-mode, --model, --alt-model, --fast, --credential-source,
  --credential-role) — none of these were enumerated for either lane.
- Telegram QA: 8 scenario ids
  (telegram-canary/-mention-gating/-mentioned-message-reply/-help-command/
  -commands-command/-tools-compact-command/-whoami-command/-context-command),
  output artifact paths (telegram-qa-report.md, -summary.json,
  -observed-messages.json), and the redaction toggle.
- Discord QA: 3 scenario ids
  (discord-canary/-mention-gating/-native-help-command-registration), output
  artifact paths, and the SUT-application-id-must-match-bot-user-id check.
- Convex credential pool: documents Discord support (only Telegram was
  mentioned before) and the per-kind payload shapes for the
  admin/add validator. Cross-links to testing.md for the broker endpoint
  contract.

Slims the duplicate Operator-flow paragraphs for Telegram and Discord into a
single one-block pointer that links to the new reference section.
2026-04-27 13:48:03 -04:00
Peter Steinberger
b39d80835f test: retry transient openai websocket live stream 2026-04-27 18:43:45 +01:00
openclaw-test-performance-agent[bot]
2f909b0b21 test: optimize slow tests 2026-04-27 17:42:22 +00:00
Gustavo Madeira Santana
dd1a94f089 docs(qa): reorg, audit against code, and refresh stale content
Reorg
- Rename the architecture page title to "QA overview" (slug stays
  /concepts/qa-e2e-automation so inbound links keep working).
- Move "Adding a channel to QA" + scenario-helper-name reference from
  testing.md into qa-e2e-automation.md under "Transport adapters". Architecture
  belongs with the architecture page.
- Drop the duplicate live-transport coverage table from testing.md; canonical
  copy stays in qa-e2e-automation.md under a new "Live transport coverage"
  heading so qa-matrix.md can deep-link to it.
- Slim testing.md QA-specific runners section to ops only, with cross-links.

Audit (against extensions/qa-lab/src/cli.ts, qa-channel/src/config-schema.ts,
and live-transport runtimes)
- qa-e2e-automation.md gains a "Command surface" table covering all 14
  openclaw qa <subcommand> forms; previously only ~7 of 14 were named.
- Document missing OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT and
  OPENCLAW_QA_DISCORD_CAPTURE_CONTENT env vars (Matrix already had it).
- Cross-link qa coverage from the Reporting section.
- qa-channel.md completes the config-key list (enabled, name, accounts,
  defaultAccount were missing from the schema doc) and pollTimeoutMs range.
- Drop stale "Follow-up work" framing in qa-channel.md (provider/model matrix,
  scenario discovery, orchestration) — all three already shipped.
- Replace "vertical slice" language with current behavior; fix misplaced
  debugger-UI paragraph.

Discoverability
- Add a Note callout to testing.md pointing at the three QA pages
  (QA overview, Matrix QA, QA channel) so maintainers landing on testing.md
  see the QA stack in the prologue.

Glossary entries for the renamed/new doc titles.
2026-04-27 13:40:11 -04:00
Gustavo Madeira Santana
abca187df5 docs(qa): add dedicated Matrix QA reference page
Adds a focused reference for the Docker-backed Matrix QA lane (CLI flags,
seven scenario profiles, eight env vars including the redaction toggle and
Tuwunel image override, scenario taxonomy, output artifact layout, and triage
tips). Source-of-truth checked against extensions/qa-matrix/src/cli.ts,
shared/live-transport-cli.ts, runners/contract/{runtime,scenario-catalog}.ts,
and substrate/harness.runtime.ts.

Registered in docs/docs.json alongside QA E2E automation.
2026-04-27 13:40:11 -04:00
Val Alexander
ea92dc9202 chore(coven): enable ClawHub publishing
Mark the Coven ACP runtime bridge as a ClawHub-publishable code plugin and align its workspace lockfile metadata.
2026-04-27 12:35:34 -05:00
Peter Steinberger
8f8ba8af40 test: harden live release validation flakes 2026-04-27 18:32:31 +01:00
Vincent Koc
a2ec5a7d72 fix(plugins): break metadata snapshot cycle 2026-04-27 10:29:27 -07:00
Peter Steinberger
147752ecc3 refactor: split plugin metadata normalizer 2026-04-27 18:27:29 +01:00
Val Alexander
e3ad82d86d fix(control-ui): polish tweakcn theme imports
Summary:
- Improve Control UI tweakcn theme import parsing and labeling.
- Apply imported theme names consistently across appearance controls.
- Document tweakcn share link and slug import flows.

Verification:
- pnpm test ui/src/ui/custom-theme.test.ts ui/src/ui/views/config.browser.test.ts ui/src/ui/views/config-quick.test.ts ui/src/ui/app-settings.test.ts ui/src/ui/storage.node.test.ts
- pnpm check:changed
- pnpm --dir ui build
2026-04-27 12:24:14 -05:00
Val Alexander
fc8ccde542 feat(acp): add opt-in Coven runtime bridge
Add the opt-in Coven ACP runtime bridge as a bundled extension while keeping ACPX as the default path.

Security hardening included before merge:
- fail closed by default instead of silently falling back;
- bounded health/socket requests and daemon response sizes;
- fixed Coven socket trust anchor and symlink/path validation;
- reject untrusted harness/session/event ids before exposing them;
- sanitize daemon-controlled terminal/status/error strings;
- use incremental event polling with bounded dedupe state;
- clean up launched Coven sessions before fallback when daemon ids are invalid.

Validation:
- pnpm test extensions/coven/src/config.test.ts extensions/coven/src/client.test.ts extensions/coven/src/runtime.test.ts
- pnpm check:changed
- GitHub CI green on a64eac20b9
- Greptile Review green
2026-04-27 12:22:29 -05:00
Peter Steinberger
fd6e1c089b test: accept current codex status wording 2026-04-27 18:11:19 +01:00
Shakker
5531502cb0 fix: normalize provider metadata live filters 2026-04-27 18:04:54 +01:00
Shakker
4cd2cabe7f fix: include cli backend owners in provider metadata filters 2026-04-27 18:04:54 +01:00
Shakker
498af508d0 fix: avoid default workspace metadata for agent models 2026-04-27 18:04:54 +01:00
Shakker
51c7f544f3 fix: reject unscoped workspace plugin metadata 2026-04-27 18:04:54 +01:00
Shakker
4ceae8262f fix: scope model provider discovery metadata to workspace 2026-04-27 18:04:54 +01:00
Shakker
4e7de4b5c9 feat: reuse current plugin metadata for provider discovery 2026-04-27 18:04:54 +01:00
Shakker
a478ab3dfa refactor: let provider discovery reuse plugin metadata 2026-04-27 18:04:54 +01:00
Peter Steinberger
f20a295782 test: align release validation expectations 2026-04-27 17:46:31 +01:00
Vincent Koc
efc3a52947 fix(sessions_spawn): tolerate ACP-only fields for subagent runtime
Preserve contributor credit and land the narrowed sessions_spawn ACP-field handling with follow-up transcript redaction and ACP resume ownership hardening. Targeted Blacksmith validation passed for the touched sessions/ACP tests.
2026-04-27 09:42:24 -07:00
Peter Steinberger
aeba1d6b47 test: keep stateful tests out of unit-fast 2026-04-27 17:34:05 +01:00
Shakker
be0c1a9835 test: update model list suppression mocks 2026-04-27 17:13:11 +01:00
Shakker
c896d42cc4 fix: keep manifest suppression on static model lists 2026-04-27 17:13:11 +01:00
Shakker
13feb1b284 fix: narrow manifest alias overrides 2026-04-27 17:13:11 +01:00
Shakker
1056a9ea81 refactor: reuse manifest catalog provider refs 2026-04-27 17:13:11 +01:00
Shakker
e535b313cd docs: document manifest alias suppression behavior 2026-04-27 17:13:11 +01:00
Shakker
df07a89b52 test: cover manifest suppression precedence 2026-04-27 17:13:11 +01:00
Shakker
6e893eaee4 refactor: expose model catalog aliases in plugin lookup 2026-04-27 17:13:11 +01:00
Shakker
03c4c319e3 feat: declare openai catalog suppressions 2026-04-27 17:13:11 +01:00
Shakker
d014b36347 feat: resolve model suppressions from manifests 2026-04-27 17:13:11 +01:00
Shakker
b2685e72c1 refactor: plan manifest catalog aliases and suppressions 2026-04-27 17:13:11 +01:00
Peter Steinberger
6d269f62d6 perf(test): route more stable tests through unit-fast 2026-04-27 17:07:29 +01:00
Shakker
b72414c94e fix: include startup plan in lookup timing 2026-04-27 17:02:57 +01:00
Shakker
94591c3cb3 fix: fingerprint plugin metadata index reuse 2026-04-27 17:02:57 +01:00
Shakker
58b4407cda fix: reject stale plugin metadata inventory 2026-04-27 17:02:57 +01:00
Shakker
197c83138e fix: reuse startup metadata in plugin bootstrap 2026-04-27 17:02:57 +01:00
Shakker
5a72378b27 fix: keep plugin metadata out of config snapshots 2026-04-27 17:02:57 +01:00
Shakker
ab28cfa9d4 fix: guard plugin metadata snapshot reuse 2026-04-27 17:02:57 +01:00
Shakker
5240422f03 docs: describe plugin metadata snapshot 2026-04-27 17:02:57 +01:00
Shakker
d62cc59388 fix: reuse startup metadata for auto enable 2026-04-27 17:02:57 +01:00
Shakker
9de2bc6ffc refactor: reuse startup plugin metadata snapshot 2026-04-27 17:02:57 +01:00
Shakker
ca4f964547 refactor: let config validation use plugin metadata snapshot 2026-04-27 17:02:57 +01:00
Shakker
440fc73448 refactor: extract plugin metadata snapshot 2026-04-27 17:02:57 +01:00
Peter Steinberger
04b5dd097d test: skip bootstrap in release validation onboarding 2026-04-27 17:01:52 +01:00
Peter Steinberger
1fd0802b88 perf(test): route more unit tests through fast lane 2026-04-27 17:01:16 +01:00
Omar Shahine
8ce4f8fc84 fix(gateway): redact SecretRef apiKey through talk.config without throwing
The talk.config discovery RPC was handing the source-snapshot's
talkProviderConfig (with the unresolved SecretRef wrapper still on
apiKey) to speechProvider.resolveTalkConfig. ElevenLabs/OpenAI's
strict normalizeResolvedSecretInputString helper threw 'unresolved
SecretRef' there, so iOS / macOS / Control UI Talk overlays never
learned the configured provider and silently fell back to local
AVSpeechSynthesizer ('robot voice') even though talk.realtime.session
and talk.speak both worked end-to-end with the same SecretRef.

Prefer the runtime-resolved provider config when calling
resolveTalkConfig, strip the apiKey field if it's still a SecretRef
wrapper at the call site, and restore the source-shaped apiKey onto
the response so the UI keeps the SecretRef context. Redaction strips
the value when includeSecrets=false.

Adds a regression test using a strict resolver speech provider that
mirrors ElevenLabs/OpenAI behavior so the path stays covered for
SecretRef apiKeys.

Fixes #72496

Thanks @omarshahine
2026-04-27 08:59:12 -07:00
Peter Steinberger
ee140ae570 perf(test): route memory package tests through unit-fast 2026-04-27 16:43:55 +01:00
Val Alexander
1cf68b9243 fix(control-ui): keep google talk off webrtc
Keep Google Live Talk browser sessions on the supported WebSocket/gateway-relay paths instead of falling back to browser WebRTC, remove stale browser-native voice controls that bypass Talk/TTS provider settings, and harden the Google Live URL plus realtime relay resource controls.

Verification:
- pnpm test ui/src/ui/realtime-talk.test.ts ui/src/ui/realtime-talk-google-live.test.ts src/gateway/talk-realtime-relay.test.ts src/gateway/server-methods/talk.test.ts
- pnpm check:changed
2026-04-27 10:35:34 -05:00
Peter Steinberger
1560e26f3d fix(ci): align yuanbao channel catalog contract 2026-04-27 16:32:21 +01:00
Peter Steinberger
56fa69a48a fix(ci): pin yuanbao official channel catalog source 2026-04-27 16:28:56 +01:00
Peter Steinberger
32bbb5b18f test: harden release validation smokes 2026-04-27 16:28:44 +01:00
cxy
5ccf179a34 feat(qqbot): group chat support, C2C streaming, chunked media upload, and architecture refactor (#70624)
* feat(qqbot): implement unified media upload handling and introduce chunked upload support

This commit enhances the media upload functionality by introducing a unified `sendMedia` method that consolidates the previous separate methods for sending images, voice messages, videos, and files. Key changes include:

- Added `uploadChunked` function for future chunked media uploads, currently marked as not implemented.
- Introduced `MediaSource` abstraction to handle various media types (URLs, base64, local files, buffers) uniformly.
- Updated existing media handling logic to utilize the new `sendMedia` method, ensuring consistent media processing across different types.
- Enhanced error handling and validation for media uploads, including MIME type checks and file size limits.

These changes aim to streamline the media upload process and prepare for future enhancements in handling larger files through chunked uploads.

* feat(qqbot): enhance media upload capabilities with chunked upload support

This commit updates the media upload functionality by implementing chunked upload support for larger files. Key changes include:

- Revised the `SKILL.md` documentation to clarify media file size limits and local file path requirements.
- Introduced a new test suite for the chunked media upload functionality, ensuring robust error handling and upload processes.
- Updated the media handling logic to enforce per-file-type upload ceilings, allowing for seamless integration of chunked uploads.
- Enhanced error handling for daily upload limits, providing user-friendly messages when limits are exceeded.

These improvements aim to streamline the media upload process and accommodate larger files effectively.

* feat(qqbot): add C2C streaming API support for message delivery

This commit introduces support for the QQ C2C official `stream_messages` API, enabling single-message typing-style updates. Key changes include:

- Updated the configuration schema to include a new `c2cStreamApi` boolean option for enabling the C2C streaming API.
- Enhanced the `QQBotAccountConfig` interface to accommodate the new streaming option.
- Implemented a `StreamingController` to manage the lifecycle of C2C stream messages, ensuring proper handling of media tags and message boundaries.
- Updated the outbound dispatch logic to utilize the new streaming capabilities, allowing for more dynamic message delivery in one-to-one chats.

These enhancements aim to improve the responsiveness and interactivity of message delivery within the QQBot framework.

* feat(qqbot): implement group chat support and unify adapter/DI architecture

- Implement group message history tracking with pending history buffer
  (record on skip, render on @-mention reply)
- Add mention detection and gating: explicit @bot, implicit quote-reply,
  ignoreOtherMentions, configurable activation mode (mention/always)
- Add group activation resolution with session store persistence
- Add message queue with per-peer FIFO and group message merging
  (batch multiple rapid messages into one merged payload)
- Add deliver debounce to merge rapid outbound text bursts into
  single messages, with media flush and maxWait cap
- Add group config resolution: per-group prompt, history limit,
  wildcard and specific group overrides
- Enrich history attachments with local paths from processAttachments
  so that history context renders downloaded paths instead of ephemeral
  QQ CDN URLs

- Merge ports/ directory into adapter/ as single entry point
- Expand EngineAdapters to 5 required ports: history, mentionGate,
  audioConvert, outboundAudio, commands
- Remove global register/get singletons in favor of constructor
  injection and one-time init
- Add createEngineAdapters() in bridge/gateway.ts as single assembly point

- Extract monolithic buildInboundContext into 11 discrete stages:
  access, content, quote, refidx, group-gate, envelope, assembly
- Extract group chat modules: history, mention, activation,
  message-gating, deliver-debounce
- Extract config/group.ts, utils/attachment-tags.ts

* feat(qqbot): add /bot-streaming command for C2C message streaming control

This commit introduces the `/bot-streaming` command, allowing users to enable or disable streaming for message delivery in C2C chats. Key changes include:

- Implementation of the `isStreamingConfigEnabled` function to check the current streaming configuration.
- Command handler for `/bot-streaming` that provides usage instructions and manages the streaming state.
- Updates to the command's response messages to inform users of the current streaming status and how to toggle it.

These enhancements aim to improve user experience by providing a straightforward way to manage streaming message delivery in private chats.

* feat(qqbot): extract interaction handler and add remote config query/update support

- Extract INTERACTION_CREATE handler from gateway.ts into a dedicated
  interaction-handler.ts module for better separation of concerns
- Add config query (type=2001) and config update (type=2002) interaction
  branches that read/write claw_cfg via runtime.config API
- Register INTERACTION intent (1<<26) in FULL_INTENTS to receive
  INTERACTION_CREATE events from the gateway
- Add InteractionType constants (CONFIG_QUERY, CONFIG_UPDATE)
- Extend GatewayPluginRuntime with optional config API (loadConfig,
  writeConfigFile) for interaction handler access
- Add QQBotAccountConfigView interface for typed config field access
- Extend acknowledgeInteraction to accept optional data payload for
  rich ACK responses (e.g. claw_cfg snapshot)
- Export getFrameworkVersion from slash-commands-impl for version
  reporting in config snapshots
- Remove unused eslint-disable directive in streaming-media-send.ts

* feat(qqbot): enhance account management and logging capabilities

- Introduced `toGatewayAccount` function to map resolved QQBot accounts to the engine's gateway account structure.
- Added `persistAccountCredentialSnapshot` function to streamline credential backup during gateway events.
- Updated the `qqbotPlugin` to utilize the new account mapping and credential persistence functions, improving the handling of account data.
- Enhanced logging functionality by modifying the `EngineLogger` interface to support metadata in log messages.
- Implemented new commands for managing logs and clearing storage, providing users with better control over their data and system resources.
- Registered multiple built-in commands for improved user interaction, including `/bot-logs` for exporting logs and `/bot-clear-storage` for managing downloaded files.
- Updated configuration schemas to reflect new options and improve clarity for users.

* fix(qqbot): resolve oxlint errors and update raw-fetch allowlist

- Replace unnecessary `else` after `return` in outbound-media-send.ts (6 occurrences)
- Use `Number.parseInt` instead of global `parseInt` in outbound.ts and streaming-media-send.ts
- Use `Number.isNaN` instead of global `isNaN` in register-basic.ts
- Prefer `**` over `Math.pow` in media-chunked.ts
- Convert interface with call signature to function type in commands.port.ts
- Update api-client.ts allowlist line number (108→124) and add media-chunked.ts:552 to raw-fetch allowlist

* docs(qqbot): translate streaming-c2c.ts header comments to English

* feat(qqbot): add voiceMediaTypes

* feat: restore dispatch changes

* fix(qqbot): align test files with updated engine interfaces after rebase

- inbound-attachments.test: replace removed registerAudioConvertAdapter
  with AudioConvertPort, pass audioConvert in ProcessContext
- inbound-pipeline.self-echo.test: add required adapters field to
  InboundPipelineDeps mock (history, mentionGate, audioConvert,
  outboundAudio, commands)
- outbound-dispatch.test: add required skipped field to InboundContext

* fix(qqbot): update test assertions to match refactored engine interfaces

- inbound-pipeline.self-echo.test: self-echo blocking was moved upstream;
  update test to expect non-blocked pipeline behavior
- outbound-dispatch.test: TTS voice path now uses unified sendMedia
  instead of sendVoiceMessage; add sendMedia mock and update assertion
- format-ref-entry.test: attachment format changed from [image: ...]
  to MEDIA: tag syntax via renderAttachmentTags; update expected output

* refactor(qqbot): migrate from deprecated config API to current/replaceConfigFile

Replace all usages of deprecated runtime config methods:
- loadConfig() → current()
- writeConfigFile(cfg) → replaceConfigFile({ nextConfig, afterWrite })

Updated files:
- bridge/narrowing.ts: writeOpenClawConfigThroughRuntime
- adapter/commands.port.ts: ApproveRuntimeGetter type signature
- commands/builtin/register-approve.ts: loadExecConfig, writeExecConfig, reset
- commands/builtin/register-streaming.ts: config read/write
- gateway/interaction-handler.ts: config query/update handlers
- gateway/types.ts: GatewayPluginRuntime.config interface

* feat(qqbot): update package.json

* fix(qqbot): replace deprecated config-runtime import with config-types subpath

Bundled plugin lint requires focused plugin-sdk subpaths.
- gateway.ts: openclaw/plugin-sdk/config-runtime → config-types
- narrowing.ts: openclaw/plugin-sdk/config-runtime → config-types

* feat(qqbot): group chat support, C2C streaming, chunked media upload, and architecture refactor (#70624) (thanks @cxyhhhhh)

---------

Co-authored-by: Bobby <zkd8907@live.com>
Co-authored-by: sliverp <870080352@qq.com>
2026-04-27 23:19:12 +08:00
Peter Steinberger
8304635258 perf(test): route speech provider registry through unit-fast 2026-04-27 16:16:12 +01:00
loongfay
3120401f53 feat(channel) yuanbao (#72756)
* feat(channel) yuanbao

* feat(channel) yuanbao

* docs(changelog): note Yuanbao channel plugin (#72756) (thanks @loongfay)

---------

Co-authored-by: loongzhao <loongzhao@tencent.com>
Co-authored-by: sliverp <870080352@qq.com>
2026-04-27 23:04:33 +08:00
Peter Steinberger
c41126dbbb ci: capture dispatched full validation runs 2026-04-27 15:51:03 +01:00
Peter Steinberger
708b42c4dc docs(changelog): link proxy fix to issue 2026-04-27 15:44:37 +01:00
Peter Steinberger
dc859584a3 fix(gateway): honor all_proxy in env dispatcher 2026-04-27 15:36:12 +01:00
Shakker
fd6c9fc7f5 fix: reuse plugin registry during config validation 2026-04-27 15:35:39 +01:00
Peter Steinberger
42fc176093 test: isolate speech provider registry mocks 2026-04-27 15:30:21 +01:00
Shakker
246fd9d3c0 fix: preserve manifest fallback for derived provider indexes 2026-04-27 15:29:11 +01:00
Shakker
7f316b917b docs: add model source plan changelog 2026-04-27 15:29:11 +01:00
Shakker
4fe7303a1f test: cover model list source planning 2026-04-27 15:29:11 +01:00
Shakker
25dda844b7 refactor: use source plan for models list 2026-04-27 15:29:11 +01:00
Shakker
f5417f626c refactor: add model list source plan 2026-04-27 15:29:11 +01:00
Peter Steinberger
d22ced122d test: isolate speech provider registry test 2026-04-27 15:28:37 +01:00
Shakker
ca444af891 fix: restore npm shims on swap failure 2026-04-27 15:27:43 +01:00
Shakker
2186080963 fix: stage npm updates under global root 2026-04-27 15:27:43 +01:00
Shakker
b0127b9f1f fix: harden npm update staging 2026-04-27 15:27:43 +01:00
Shakker
6985c6751c fix: make npm global updates atomic 2026-04-27 15:27:43 +01:00
Peter Steinberger
9b4c1f0fa3 test: update compaction token test contexts 2026-04-27 15:18:28 +01:00
Peter Steinberger
467ee701ef fix(ci): align tests with runtime barrels 2026-04-27 15:15:07 +01:00
Peter Steinberger
9090457da7 test(plugin-sdk): use narrow config runtime mocks 2026-04-27 15:14:02 +01:00
Peter Steinberger
a2af8054e1 test: harden live release checks 2026-04-27 15:11:46 +01:00
Peter Steinberger
016a0b4de9 fix(gateway): avoid echoing rotated device tokens 2026-04-27 15:10:05 +01:00
Peter Steinberger
dacf43640a fix(ci): repair main test gates 2026-04-27 15:03:39 +01:00
Peter Steinberger
a9648664c1 perf(test): route memory dreaming through unit-fast 2026-04-27 15:03:21 +01:00
Peter Steinberger
22e2e45c57 fix(cli): skip respawn for foreground gateway 2026-04-27 15:01:33 +01:00
Peter Steinberger
d69eeeb2a8 fix: skip test-only plugin install scan findings 2026-04-27 15:00:55 +01:00
Peter Steinberger
82b4049744 refactor: narrow discord slack runtime api barrels 2026-04-27 15:00:03 +01:00
Peter Steinberger
75a96bafcf docs: fix changelog attribution credits 2026-04-27 15:00:03 +01:00
Peter Steinberger
4336a7f3a9 refactor(plugin-sdk): narrow config runtime imports 2026-04-27 14:58:32 +01:00
Peter Steinberger
f3e8a8a319 fix(agents): persist compaction token snapshots 2026-04-27 14:58:15 +01:00
Peter Steinberger
f9946eb069 fix(memory): parse qmd vector status variants 2026-04-27 14:57:28 +01:00
Peter Steinberger
1f7b7c249a fix(google-meet): grant browser media permissions 2026-04-27 14:54:07 +01:00
Peter Steinberger
713cc74bff fix: quiet installed plugin override warnings 2026-04-27 14:53:36 +01:00
Peter Steinberger
2e99c1d227 fix(subagents): enforce explicit spawn allowlists 2026-04-27 14:53:17 +01:00
Peter Steinberger
58a4ca4423 refactor: narrow whatsapp runtime api barrel 2026-04-27 14:52:21 +01:00
Peter Steinberger
1ed6d04014 ci: guard plugin sdk wildcard reexports 2026-04-27 14:52:21 +01:00
Peter Steinberger
877b5a14f1 fix(sessions): batch store cap maintenance 2026-04-27 14:51:53 +01:00
Peter Steinberger
0ac0357486 docs(memory): explain qmd collection compatibility 2026-04-27 14:44:01 +01:00
Peter Steinberger
63011fcbb0 ci: update generated protocol swift models 2026-04-27 14:42:27 +01:00
Peter Steinberger
e035300d8e fix(acp): allow manual spawn with dispatch paused 2026-04-27 14:40:12 +01:00
Peter Steinberger
c3b3da41fe fix: allow trusted openclaw peer symlinks 2026-04-27 14:40:02 +01:00
Peter Steinberger
cbf6ed2b35 chore: publish OpenClaw 2026.4.25 appcast 2026-04-27 14:39:56 +01:00
Peter Steinberger
4c544e649c test: move more stateful tests to unit-fast 2026-04-27 14:37:51 +01:00
Peter Steinberger
4ebec8b5dc fix(memory): group qmd collection searches 2026-04-27 14:37:12 +01:00
Peter Steinberger
eb1a201060 refactor: narrow media core plugin api barrels 2026-04-27 14:34:00 +01:00
Peter Steinberger
0f996ad4b0 ci: enforce changelog attribution policy in pr gates 2026-04-27 14:33:59 +01:00
Peter Steinberger
ad0f600450 fix(gateway): avoid systemd service split-brain 2026-04-27 14:32:49 +01:00
Peter Steinberger
c00ef238be docs(tools): clarify sessions_spawn profile gating 2026-04-27 14:31:54 +01:00
Peter Steinberger
23d047dff5 ci: update generated protocol models 2026-04-27 14:31:13 +01:00
Peter Steinberger
1382fb5bd7 fix(agents): fail closed missing requester completion routes 2026-04-27 14:30:59 +01:00
Peter Steinberger
6956e8406d fix: honor profile plugin install roots 2026-04-27 14:30:12 +01:00
Shakker
f88c330657 fix: preserve runtime config during source plugin activation 2026-04-27 14:29:49 +01:00
Shakker
a964dcbddb fix: honor source plugin activation at startup 2026-04-27 14:29:49 +01:00
Shakker
a88f2ba939 fix: avoid startup auto-enable runtime defaults 2026-04-27 14:29:48 +01:00
Peter Steinberger
6ced6bc4a3 ci: satisfy live shard lint 2026-04-27 14:29:41 +01:00
Peter Steinberger
bbbc80ddcc ci: guard changelog bot attributions 2026-04-27 14:29:41 +01:00
Peter Steinberger
6e8aaef1cc fix(google-meet): avoid duplicate test speech 2026-04-27 14:29:08 +01:00
Peter Steinberger
73ba282b54 docs(memory): clarify qmd mask compatibility 2026-04-27 14:26:56 +01:00
Peter Steinberger
8e09105bd3 test: route more mock-only tests through unit-fast 2026-04-27 14:24:43 +01:00
Peter Steinberger
2243a68a1d ci: shard release live validation 2026-04-27 14:24:10 +01:00
Peter Steinberger
f6bda8d36b refactor(providers): share Claude thinking profiles 2026-04-27 14:23:12 +01:00
Peter Steinberger
93bbbe5e37 feat: add browser realtime talk transports 2026-04-27 14:22:32 +01:00
Peter Steinberger
5dd1e264eb refactor(config): tighten plugin config guardrails 2026-04-27 14:20:27 +01:00
Peter Steinberger
ef9d108436 fix(gateway): include client in hello snapshot 2026-04-27 14:20:27 +01:00
Peter Steinberger
c3c8f25bab fix(memory): report qmd dirty watcher state 2026-04-27 14:20:10 +01:00
Peter Steinberger
28f264034b fix: discover symlinked plugin directories 2026-04-27 14:17:32 +01:00
sfuminya
2c57d70a10 fix: preserve requester route for subagent completion delivery (#72806)
* fix: preserve requester route for subagent completion delivery

* fix(agents): preserve requester subagent completion routes

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-27 14:17:14 +01:00
Peter Steinberger
dfd9dbe4e1 docs: clarify changelog attribution exclusions 2026-04-27 14:16:43 +01:00
Peter Steinberger
4300a6165e test: route more setup-free tests through unit-fast 2026-04-27 14:16:20 +01:00
Peter Steinberger
67a447c175 refactor: tighten plugin runtime sdk boundaries 2026-04-27 14:15:53 +01:00
Peter Steinberger
b181930c23 fix(memory): skip qmd vectors in lexical mode 2026-04-27 14:09:42 +01:00
Peter Steinberger
6a0dc3a9bc fix: cache plugin discovery realpaths 2026-04-27 14:09:15 +01:00
Peter Steinberger
9ca4049861 ci: match package Telegram harness to release ref 2026-04-27 14:06:05 +01:00
Peter Steinberger
52a1cbc1c6 fix(qa-lab): keep gateway client on generic sdk seam 2026-04-27 14:05:09 +01:00
Peter Steinberger
57401f1581 fix(google-meet): use OpenClaw browser for local joins 2026-04-27 14:03:46 +01:00
Peter Steinberger
8de458c6c0 fix(qa-lab): use generic gateway runtime SDK 2026-04-27 14:03:28 +01:00
harish ganeshmurthy
f75d8827f2 fix(opencode): expose Claude thinking levels (#72778)
* fix(opencode): expose claude thinking levels

* test(opencode): cover adaptive claude thinking bounds

* docs(changelog): credit opencode thinking contributor

---------

Co-authored-by: haishmg <4529977+haishmg@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-27 14:03:26 +01:00
Peter Steinberger
1b1916053f ci: inline Docker release planning for old refs 2026-04-27 14:03:17 +01:00
Peter Steinberger
fd4b59a906 ci: keep release checks compatible with stable refs 2026-04-27 13:59:49 +01:00
Peter Steinberger
d0e4472616 test(vitest): keep speech registry out of unit-fast 2026-04-27 13:57:15 +01:00
Peter Steinberger
f15c9f1d5f test: move more setup-free tests to unit-fast 2026-04-27 13:56:21 +01:00
Peter Steinberger
df65a75f92 fix(memory): avoid live embedding probes in status 2026-04-27 13:55:51 +01:00
Peter Steinberger
dc495e6d62 refactor(discord): isolate model picker apply flow 2026-04-27 13:50:43 +01:00
Peter Steinberger
951a0d89d8 fix(discord): persist stale model picker overrides 2026-04-27 13:50:43 +01:00
Peter Steinberger
1fbe83d09f fix: keep link understanding from dropping replies 2026-04-27 13:45:05 +01:00
Peter Steinberger
fa1f670716 test: route setup-free tests through unit-fast 2026-04-27 13:42:32 +01:00
Peter Steinberger
770978b8d3 build(config): refresh generated schema baseline 2026-04-27 13:42:12 +01:00
Peter Steinberger
8e37ee4bf2 fix(voice-call): avoid blocking gateway startup 2026-04-27 13:40:30 +01:00
Peter Steinberger
2d90dbe512 docs: add release feature redirect URLs 2026-04-27 13:40:00 +01:00
Peter Steinberger
f3528e7755 fix(openrouter): retire stealth model catalog entries 2026-04-27 13:36:49 +01:00
Peter Steinberger
9cde9261c6 docs(changelog): move post-release entries to 2026.4.26 2026-04-27 13:34:02 +01:00
Peter Steinberger
cae492374c test: reduce repeated test setup overhead 2026-04-27 13:33:05 +01:00
Peter Steinberger
0931a1f11e ci: fix release validation dispatch and protocol drift 2026-04-27 13:32:03 +01:00
Peter Steinberger
d9b8001502 build(protocol): refresh swift gateway models 2026-04-27 13:31:15 +01:00
Peter Steinberger
252c63429e fix(providers): map native reasoning efforts 2026-04-27 13:27:58 +01:00
Peter Steinberger
4119d65e82 test(doctor): keep repair sequencing unit isolated 2026-04-27 13:27:36 +01:00
Peter Steinberger
3c6d178f4e docs: clarify malformed plugin tool guards 2026-04-27 13:27:19 +01:00
Peter Steinberger
41d5c27894 fix(docker): install runtime ca certificates 2026-04-27 13:24:42 +01:00
Peter Steinberger
98b441edb1 ci: split release docker integration chunks 2026-04-27 13:24:30 +01:00
Peter Steinberger
750c180a6c fix(ollama): warn on WSL2 CUDA crash loop risk 2026-04-27 13:24:04 +01:00
Peter Steinberger
0a076bc0fc fix: isolate malformed plugin tools 2026-04-27 13:22:28 +01:00
Peter Steinberger
7fb2a356e8 fix(nodes): allow removing stale paired nodes 2026-04-27 13:20:52 +01:00
Peter Steinberger
400be3b63f test(agents): align failure-kind protocol expectations 2026-04-27 13:18:16 +01:00
Peter Steinberger
4bd356d03a fix(channels): clarify message target syntax 2026-04-27 13:18:04 +01:00
Peter Steinberger
6fe9285f64 fix(ci): sync locale and cron contract tests 2026-04-27 13:15:59 +01:00
Peter Steinberger
cff1bdb491 ci: trim duplicate release package lanes 2026-04-27 13:15:10 +01:00
Peter Steinberger
4260bb0418 fix: quarantine invalid plugin configs 2026-04-27 13:14:59 +01:00
Alex Knight
b1e530b204 fix(cli): mark embedded agent fallback (#72730)
* fix(cli): mark embedded agent fallback

* refactor(cli): structure embedded fallback metadata

* refactor(cli): move fallback metadata types out of EmbeddedPiRunMeta

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-04-27 22:14:11 +10:00
github-actions[bot]
bef28fcf1a chore(ui): refresh th control ui locale 2026-04-27 12:13:31 +00:00
Peter Steinberger
7e45272319 test(ci): align docker scenario guard with sharded sweep 2026-04-27 13:13:04 +01:00
github-actions[bot]
ef87620c5b chore(ui): refresh id control ui locale 2026-04-27 12:12:56 +00:00
github-actions[bot]
973fbcd65b chore(ui): refresh pl control ui locale 2026-04-27 12:12:51 +00:00
github-actions[bot]
a3ef1938b6 chore(ui): refresh uk control ui locale 2026-04-27 12:12:39 +00:00
github-actions[bot]
6c0d9b1642 chore(ui): refresh tr control ui locale 2026-04-27 12:12:32 +00:00
github-actions[bot]
e7d3cfa7ca chore(ui): refresh fr control ui locale 2026-04-27 12:11:55 +00:00
github-actions[bot]
3769a93752 chore(ui): refresh ko control ui locale 2026-04-27 12:11:51 +00:00
github-actions[bot]
1633e38a77 chore(ui): refresh ja-JP control ui locale 2026-04-27 12:11:46 +00:00
github-actions[bot]
1a466d5a44 chore(ui): refresh es control ui locale 2026-04-27 12:11:42 +00:00
github-actions[bot]
1ad36486b8 chore(ui): refresh zh-CN control ui locale 2026-04-27 12:11:01 +00:00
github-actions[bot]
3352f8a569 chore(ui): refresh zh-TW control ui locale 2026-04-27 12:10:55 +00:00
github-actions[bot]
61633b5ca7 chore(ui): refresh pt-BR control ui locale 2026-04-27 12:10:52 +00:00
github-actions[bot]
450eae0ecf chore(ui): refresh de control ui locale 2026-04-27 12:10:48 +00:00
Peter Steinberger
0e586bb48a fix(agents): improve fallback failure observability 2026-04-27 13:10:12 +01:00
Peter Steinberger
63eaf8ea51 fix(models): default local custom providers to completions 2026-04-27 13:09:59 +01:00
Peter Steinberger
b6c8e51dcb fix(gateway): build hello snapshot after presence update 2026-04-27 13:09:30 +01:00
Peter Steinberger
3517b25482 fix: remove duplicate hello snapshot build 2026-04-27 13:09:16 +01:00
Peter Steinberger
c6ebd99a46 fix(control-ui): surface lazy panel load failures 2026-04-27 13:09:02 +01:00
Peter Steinberger
0141471dd5 refactor: move shared helpers off reserved sdk seams 2026-04-27 13:07:54 +01:00
Peter Steinberger
e91f9a3f67 fix: include connected client in hello snapshot 2026-04-27 13:07:45 +01:00
Peter Steinberger
fef4b57b39 fix(gateway): include connected client in hello snapshot 2026-04-27 13:06:30 +01:00
Peter Steinberger
f68ef1ae7c ci: shard bundled plugin release sweep 2026-04-27 13:05:14 +01:00
Peter Steinberger
0dfea099d6 test: speed up focused test setup 2026-04-27 13:00:43 +01:00
Peter Steinberger
e9986aa787 fix(ci): make full validation rerun-aware 2026-04-27 13:00:09 +01:00
Peter Steinberger
6a55a00da4 fix(agents): scope loop detection to runs 2026-04-27 12:59:54 +01:00
Peter Steinberger
d73e2ee774 fix(google-meet): use PCM audio for Chrome realtime 2026-04-27 12:55:00 +01:00
Val Alexander
27a4bba90a fix(ui): render cron markdown summaries
## Summary
- render cron job prompts and run summaries through the sanitized markdown pipeline in the Control UI
- keep system-event cron payloads plain and prevent markdown link clicks from triggering row selection
- handle failed runs with missing or empty summaries without duplicating or hiding the error text

## Verification
- pnpm test ui/src/ui/views/cron.test.ts
- pnpm test src/plugins/doctor-contract-registry.test.ts src/plugins/setup-registry.test.ts
- pnpm check:changed
- GitHub CI green on 251f01a3b0
2026-04-27 06:53:51 -05:00
Peter Steinberger
769d04b4ce docs(models): clarify local chat completions routing 2026-04-27 12:53:46 +01:00
Peter Steinberger
10257114ac test: speed up focused unit tests 2026-04-27 12:52:54 +01:00
Peter Steinberger
a041ea7ca7 docs(plugins): clarify runtime config access 2026-04-27 12:52:20 +01:00
Peter Steinberger
9d5a211019 refactor(plugins): enforce config API deprecations 2026-04-27 12:52:20 +01:00
Peter Steinberger
94a9d3f0be refactor(config): track runtime config revisions 2026-04-27 12:52:20 +01:00
Peter Steinberger
047c03cc88 fix(gateway): drop stale webchat handshakes 2026-04-27 12:51:17 +01:00
Peter Steinberger
eaae63d288 refactor: keep plugin sdk owner seams explicit 2026-04-27 12:50:31 +01:00
Peter Steinberger
189535308f test: align plugin jiti Windows expectations 2026-04-27 12:49:00 +01:00
Peter Steinberger
22a51de422 fix: tolerate stale channel plugin config 2026-04-27 12:48:13 +01:00
Peter Steinberger
c0ea89cfd2 fix(agents): recover unclosed reasoning-only replies 2026-04-27 12:45:11 +01:00
Peter Steinberger
74fb6be716 fix(ui): scope agent identity to active session
Co-authored-by: Sahil Satralkar <62758655+sahilsatralkar@users.noreply.github.com>
2026-04-27 12:45:00 +01:00
Peter Steinberger
d25dd7c2bd test: cache dockerfile fixture reads 2026-04-27 12:42:29 +01:00
Vincent Koc
9be54044eb docs(doctor): document stale channel plugin cleanup (edb3e84898)
Trace to edb3e84898 (fix: clean stale plugin channel config). When
openclaw doctor --fix removes a missing channel plugin, it also cascades
the cleanup to dangling channel config, heartbeat targets, and channel
model overrides, preventing gateway boot loops after failed plugin
reinstalls. Added an Accordion 11d to docs/gateway/doctor.md listing the
exact config keys that get pruned alongside the plugin entry.
2026-04-27 04:40:49 -07:00
Peter Steinberger
9b2f10dcf8 fix(agents): preserve distinct empty exec failures 2026-04-27 12:40:41 +01:00
martingarramon
4f50921e0f fix(gateway/schema): require hello-ok auth
Fixes #68160.

Drops stale optionality from the hello-ok auth schema and keeps generated Swift models, macOS fixtures, browser client types, protocol docs, and merged-base test boundaries aligned.
2026-04-27 06:40:36 -05:00
Peter Steinberger
00d4099526 fix(discord): inherit thread model overrides without transcript fork 2026-04-27 12:40:32 +01:00
Peter Steinberger
b056d594b4 fix(plugins): normalize Windows Jiti paths 2026-04-27 12:39:21 +01:00
Vincent Koc
c85065eb7f fix(cli): tighten Windows restart policy-close health checks
Preserve contributor credit and land the narrowed restart-health fix after ProjectClownfish review/follow-up.
2026-04-27 04:38:29 -07:00
Peter Steinberger
3da6d6ee18 fix(qwen): use plugin test boundary helpers 2026-04-27 12:36:50 +01:00
Peter Steinberger
c59af3caf7 docs(plugins): document runtime config APIs 2026-04-27 12:35:59 +01:00
Peter Steinberger
7f3f108521 refactor(config): migrate plugin config access 2026-04-27 12:35:58 +01:00
Peter Steinberger
48ebed3ed3 fix(plugins): normalize bundled sidecar jiti imports 2026-04-27 12:35:51 +01:00
Peter Steinberger
da8576c0bf test: guard plugin boundary classifications 2026-04-27 12:35:43 +01:00
Peter Steinberger
7ec97c010c test: speed up plugin activation boundary test 2026-04-27 12:35:31 +01:00
Vincent Koc
727927aae0 fix(docker): repair named-volume state directory ownership
Preserve contributor credit and land the narrowed Docker ownership fix after ProjectClownfish review/follow-up.
2026-04-27 04:34:35 -07:00
Peter Steinberger
e9bce3f81c fix(agents): stabilize exec loop outcome hashing 2026-04-27 12:33:37 +01:00
Peter Steinberger
35335214b3 fix(compaction): avoid preserving duplicate user turns 2026-04-27 12:30:59 +01:00
Peter Steinberger
dae09d26b9 test(live): tolerate provider-specific live probe variance 2026-04-27 12:30:12 +01:00
Peter Steinberger
053aff6d35 fix(mcp): normalize streamable http server aliases 2026-04-27 12:29:24 +01:00
Peter Steinberger
3da4b28d1b fix(agents): avoid overload classification for live model switches 2026-04-27 12:28:33 +01:00
Peter Steinberger
82e164c018 test: speed up acp rate-limit coverage 2026-04-27 12:28:09 +01:00
Peter Steinberger
db087a4be7 fix(doctor): stream bundled runtime dep repair progress 2026-04-27 12:27:44 +01:00
Shakker
05fce28ec0 docs: document installed manifest fallback cache 2026-04-27 12:26:10 +01:00
Peter Steinberger
7363fb4a44 refactor: move telegram poll visibility out of core 2026-04-27 12:25:57 +01:00
Peter Steinberger
3bc29dd604 fix(sqlite): bound WAL sidecar growth 2026-04-27 12:25:10 +01:00
Peter Steinberger
bbfdb38e4e fix: show doctor runtime dependency install progress 2026-04-27 12:25:05 +01:00
Peter Steinberger
5afa24a9fc fix(qwen): preserve custom modelstudio providers 2026-04-27 12:24:25 +01:00
Peter Steinberger
dca9fa471f fix(ui): preserve session assistant identity 2026-04-27 12:20:36 +01:00
Shakker
6f6e2765e2 test: reset installed manifest cache in web search provider tests 2026-04-27 12:19:51 +01:00
Shakker
ac7aef6c5b docs: frame installed manifest cache as fallback 2026-04-27 12:19:51 +01:00
Marcus Castro
b7a1bfd2d7 fix(plugins): cache installed manifest registry 2026-04-27 12:19:51 +01:00
Peter Steinberger
e59e0393f5 fix(acpx): mark claude acp package test-only 2026-04-27 12:18:59 +01:00
Peter Steinberger
da822a56d8 refactor(vllm): own nemotron thinking payloads 2026-04-27 12:15:54 +01:00
Peter Steinberger
22bb53ac9a docs(changelog): note tool cache channel invalidation 2026-04-27 12:14:51 +01:00
Peter Steinberger
2cfe6bf4e5 fix(ollama): dedupe latest models during setup 2026-04-27 12:14:10 +01:00
Peter Steinberger
78577ac147 fix: route tasks json through lean cli path 2026-04-27 12:13:51 +01:00
Peter Steinberger
e20f755ac5 fix(gateway): invalidate tool inventory on channel registry changes 2026-04-27 12:13:39 +01:00
Peter Steinberger
277cc640b1 fix(acp): wait for claude results before idle completion 2026-04-27 12:12:48 +01:00
Peter Steinberger
eebdda92f0 fix(media): keep audio input repair in doctor 2026-04-27 12:12:41 +01:00
Peter Steinberger
e98f976a70 refactor: centralize provider stream fallback ownership 2026-04-27 12:11:29 +01:00
清秋
8200d878a3 fix(ui): harden webchat input history behavior
Harden WebChat input history handling so draft, navigation, and render-state behavior stay consistent across the chat UI.

Validated locally on the rebased PR head 742a5f22f1:
- CI=true OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- CI=true OPENCLAW_LOCAL_CHECK=0 pnpm test:changed

Closes #38702.
2026-04-27 06:08:55 -05:00
Peter Steinberger
1971db0dc5 fix(media): expand legacy audio input placeholder 2026-04-27 12:06:58 +01:00
Peter Steinberger
8e14f5c749 fix(agents): drop malformed reasoning before orphan close tags 2026-04-27 12:06:37 +01:00
Egor Dementyev
b081b195a3 feat(hooks): emit gateway shutdown lifecycle events (#63084)
Merged via squash.

Prepared head SHA: 188d6fef24
Co-authored-by: eyev0 <22837926+eyev0@users.noreply.github.com>
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Reviewed-by: @BunsDev
2026-04-27 06:05:43 -05:00
Peter Steinberger
45bc7f69f2 fix(gateway): cache effective tool inventory 2026-04-27 12:04:40 +01:00
Peter Steinberger
496964fced test: speed up subagent announce format e2e 2026-04-27 12:03:54 +01:00
Peter Steinberger
a3144b6bfd fix(agents): preserve explicit Ollama local auth marker 2026-04-27 12:00:51 +01:00
Peter Steinberger
9dd01b5e49 fix: align plugin runtime dependency contracts 2026-04-27 11:58:28 +01:00
Peter Steinberger
9bc703213b fix(control-ui): preserve loopback client version labels 2026-04-27 11:56:58 +01:00
Peter Steinberger
7ef899ad96 test: speed up channel onboarding e2e 2026-04-27 11:55:16 +01:00
Peter Steinberger
583f32f56f test: align auth and config help expectations 2026-04-27 11:52:54 +01:00
Peter Steinberger
4f7498f6df chore: update config help baseline 2026-04-27 11:51:55 +01:00
Peter Steinberger
6ae2e9e9dc fix(gateway): keep effective tools on hot registry path 2026-04-27 11:51:15 +01:00
Peter Steinberger
9dcd53c0b6 fix(memory): avoid watchers for memory CLI commands 2026-04-27 11:50:44 +01:00
Peter Steinberger
c9b9887583 test: speed up embedded runner e2e mocks 2026-04-27 11:50:37 +01:00
Peter Steinberger
836d4b4105 refactor(vllm): own qwen thinking payloads 2026-04-27 11:50:25 +01:00
Peter Steinberger
4f7038ae33 fix(anthropic): drop prefill with thinking 2026-04-27 11:50:25 +01:00
Peter Steinberger
75c8c1bebe fix(agents): honor qwen chat-template thinking compat 2026-04-27 11:50:24 +01:00
Peter Steinberger
3db407da40 test(security): cover bundled plugin allowlist audit 2026-04-27 11:50:24 +01:00
Peter Steinberger
4a65b69073 fix: accept local markers for custom ollama providers 2026-04-27 11:47:09 +01:00
Peter Steinberger
5a81c4000c chore: tighten plugin boundary export audit 2026-04-27 11:47:09 +01:00
Peter Steinberger
236ca49998 docs: clarify memory search input type help 2026-04-27 11:47:06 +01:00
Peter Steinberger
f487ed160e test(agents): fix compatible retry fixture 2026-04-27 11:44:56 +01:00
Peter Steinberger
769994eb04 test(agents): cover compatible empty retries 2026-04-27 11:44:55 +01:00
Peter Steinberger
fd9d32f022 fix(agents): retry empty compatible turns 2026-04-27 11:44:55 +01:00
Peter Steinberger
edb3e84898 fix: clean stale plugin channel config 2026-04-27 11:41:53 +01:00
harish ganeshmurthy
fa0f7d1e73 fix(webchat): hide reset startup prompt from history
Closes #72369.

Remote validation (Blacksmith Testbox tbx_01kq7874j733m8pxesmgvfz1x1):
- pnpm test src/auto-reply/reply/get-reply-run.media-only.test.ts src/gateway/server-methods/server-methods.test.ts
- node scripts/run-vitest.mjs run --config test/vitest/vitest.unit-ui.config.ts ui/src/ui/controllers/chat.test.ts
- pnpm check:changed

Co-authored-by: haishmg <4529977+haishmg@users.noreply.github.com>
2026-04-27 11:41:33 +01:00
Peter Steinberger
ae86541364 fix: export tts runtime plugin sdk subpath 2026-04-27 11:40:56 +01:00
Vincent Koc
9ef0131e1c docs(local-models): note LAN-local auth marker support (fee16865b2 + 0dd2844991)
Trace to fee16865b2 (fix(agents): accept LAN local auth markers) and the
companion 0dd2844991 (fix: preserve Ollama local marker auth). The fix
extends ollama-local marker handling to any custom OpenAI-compatible
provider whose baseUrl resolves to loopback, a private LAN, .local, or a
bare hostname, so persisted local markers no longer fail with missing-auth
errors for non-Ollama-typed local providers (LM Studio, vLLM, LiteLLM).

The Ollama provider page already covers ollama-local for Ollama-typed
providers; this note lives in docs/gateway/local-models.md where custom
OpenAI-compatible local stacks are documented.
2026-04-27 03:39:26 -07:00
Peter Steinberger
7688b696de refactor: remove bundled plugin sdk self imports 2026-04-27 11:36:08 +01:00
Peter Steinberger
8a8cc8dc9f fix(memory): refresh tool config at execution 2026-04-27 11:36:02 +01:00
Peter Steinberger
fa468d0c2d fix(bonjour): default mdns host to system hostname 2026-04-27 11:35:19 +01:00
Vincent Koc
3a73826e28 fix(docs-sync): prune orphan locale docs whose English source no longer exists
The publish workflow rsyncs source docs/ into the publish repo with --delete,
but explicitly protects locale directories so translation files survive
non-translation-pipeline syncs. When an English source file is renamed (for
example install/migrating-matrix.md -> channels/matrix-migration.md), the
locale copies at <locale>/install/migrating-matrix.md become orphans:
deleted from the English nav but still present on disk.

Mintlify's hosted build appears to silently fall back to the previous
deployment when nav references a path with mixed locale availability, so
recent docs changes (the migration hub rework, matrix-migration move) are
not propagating to docs.openclaw.ai even though every CI run reports
success and the publish repo has the right English content.

Add a pruneOrphanLocaleDocs() pass that walks every generated-locale
directory in the publish target and removes any .md/.mdx file whose
matching English path no longer exists in source docs. Runs after rsync
and before composing docs.json so the regenerated nav and the on-disk
files stay consistent. Verified the logic against the live publish repo:
identifies all ja-JP/es/pt-BR/ko/de/fr/ar/it/tr/uk/id/pl/zh-CN orphans of
install/migrating-matrix.md (12 entries) and would also catch any future
renames the same way.
2026-04-27 03:34:57 -07:00
Peter Steinberger
ca88daad1e test(agents): keep openai image cache probe non-blocking 2026-04-27 11:34:15 +01:00
Peter Steinberger
169d33ded2 test: speed up auth rotation e2e 2026-04-27 11:33:36 +01:00
Peter Steinberger
d337fa8946 test: align build profile guard expectations 2026-04-27 11:31:57 +01:00
Peter Steinberger
f50fb73560 fix(whatsapp): honor env proxy during QR login 2026-04-27 11:30:29 +01:00
Bartok
f0b327cf68 fix(media): gate markdown image extraction by channel (#72718)
Closes #72642

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-27 11:27:35 +01:00
Peter Steinberger
775ed36c16 feat(memory): support asymmetric embedding input types 2026-04-27 11:25:40 +01:00
Peter Steinberger
0dd2844991 fix: preserve Ollama local marker auth 2026-04-27 11:25:06 +01:00
Peter Steinberger
a421e0be84 test: fix plugin registry CI contracts 2026-04-27 11:25:06 +01:00
Peter Steinberger
a0aedea63d fix: guard cli bootstrap imports 2026-04-27 11:24:35 +01:00
Peter Steinberger
fa0d81ed13 fix(agents): retry empty openai-compatible turns 2026-04-27 11:24:14 +01:00
Peter Steinberger
f820f89f14 test(agents): align local marker auth expectation 2026-04-27 11:23:56 +01:00
Peter Steinberger
f6ee2877e0 refactor: share memory dreaming cron constants 2026-04-27 11:19:09 +01:00
Peter Steinberger
9b0a0fb0a7 refactor: tighten plugin boundary surfaces 2026-04-27 11:19:09 +01:00
Peter Steinberger
c4fe72b8d6 ci: pin full release validation child refs 2026-04-27 11:16:16 +01:00
Peter Steinberger
bc0b02b2a6 fix(channels): avoid bundled plugin load paths 2026-04-27 11:15:42 +01:00
Peter Steinberger
4067d78a4c fix(exec): enforce default timeout on node runs 2026-04-27 11:15:33 +01:00
Peter Steinberger
c20bcc59a8 fix(git-hooks): skip ignored staged paths 2026-04-27 11:12:55 +01:00
Vincent Koc
0e4be1e3d3 docs(matrix): move migration guide from install/ to channels/
The Matrix migration guide is plugin-upgrade content (encrypted-state recovery,
device verification, room-key restore) rather than a cross-system import or
machine move, so it belongs alongside the Matrix channel docs rather than under
Install > Maintenance > Migrating.

- Move docs/install/migrating-matrix.md to docs/channels/matrix-migration.md
- Update inbound link in docs/channels/matrix.md
- Update the migrating.md hub: replace the Matrix Card with a one-line link in 'Upgrade a plugin in place'
- Refresh Related list on the moved page (link Matrix push rules and Migration guide hub)
- docs.json: remove install/migrating-matrix from Maintenance > Migrating, slot channels/matrix-migration between channels/matrix and channels/matrix-push-rules in the Mainstream channels group, and add a /install/migrating-matrix -> /channels/matrix-migration redirect
2026-04-27 03:12:32 -07:00
Peter Steinberger
7630322f64 docs: format migration guides 2026-04-27 11:11:27 +01:00
Peter Steinberger
6778e44333 test(exec): cover background timeout opt-out 2026-04-27 11:10:51 +01:00
Shakker
07946a404d chore: update a2ui bundle hash 2026-04-27 11:10:12 +01:00
Shakker
06de1d2080 fix: reuse web provider candidate manifests 2026-04-27 11:10:12 +01:00
Peter Steinberger
4003e4389a fix(memory-core): support dreaming model override 2026-04-27 11:08:21 +01:00
Peter Steinberger
b8a9dc9d78 test(moonshot): avoid redundant live result type 2026-04-27 11:07:21 +01:00
Peter Steinberger
9d52b615ad feat(ollama): prefix memory embedding queries 2026-04-27 11:07:20 +01:00
Peter Steinberger
92100efa04 fix(exec): honor default timeout for background runs 2026-04-27 11:06:23 +01:00
Peter Steinberger
ca882aeb42 test: remove discord sdk path references 2026-04-27 11:03:14 +01:00
Peter Steinberger
9f62c73893 fix(cron): verify delivery before clearing message warnings 2026-04-27 11:02:09 +01:00
Peter Steinberger
a4b97075ae fix: align support URL redaction 2026-04-27 11:00:42 +01:00
Peter Steinberger
5757d1bb69 ci: harden live release validation lane 2026-04-27 10:59:25 +01:00
Peter Steinberger
fee16865b2 fix(agents): accept LAN local auth markers 2026-04-27 10:57:35 +01:00
Vincent Koc
a6eb051b3a docs(migration): convert migrating.md to a hub, nest per-source guides, reorder nav
- install/migrating: convert to a hub page with three clear paths (CardGroup for cross-system imports linking Claude+Hermes, machine-to-machine move with Steps and AccordionGroup, plugin upgrade Card linking Matrix)
- install/migrating-claude: align with Hermes page structure (add Restart-and-verify Step, JSON output for automation, Troubleshooting AccordionGroup with 4 entries, cross-link to Hermes guide)
- cli/migrate: tighten intro to mention both bundled providers and link the migration hub
- docs.json: move Maintenance group to immediately after Install overview, nest the four migrating pages (migrating, migrating-claude, migrating-hermes, migrating-matrix) under a 'Migrating' subgroup so they collapse into a dropdown
2026-04-27 02:57:15 -07:00
Peter Steinberger
a0023f4978 fix(logging): redact URL query secrets 2026-04-27 10:56:47 +01:00
Peter Steinberger
1b581b4c71 fix(ci): stabilize live release validation 2026-04-27 10:56:35 +01:00
Peter Steinberger
e7432ae01d fix: redact URL query credentials in diagnostics 2026-04-27 10:55:22 +01:00
Peter Steinberger
d33eebd050 fix(cron): ignore delivered presentation warnings 2026-04-27 10:53:35 +01:00
VACInc
614a2846a2 fix: continue Google Live consult responses (#72189) (thanks @VACInc)
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
2026-04-27 10:52:00 +01:00
Peter Steinberger
8f262211ee docs(ollama): clarify qwen stability settings 2026-04-27 10:49:44 +01:00
Peter Steinberger
7dc9a367ef fix: avoid persisting proxy env in gateway services 2026-04-27 10:46:31 +01:00
Shakker
021ef1220d fix: reuse provider discovery plugin metadata 2026-04-27 10:46:09 +01:00
Peter Steinberger
c9e6f371e4 fix(memory-core): quiet request-scoped fallback 2026-04-27 10:45:55 +01:00
Peter Steinberger
dfe58a1b8e fix(agents): treat TUI client label as current session 2026-04-27 10:45:26 +01:00
ziyitan
27ee5c0098 fix(gateway): redact secrets in skills.update response (#69998)
Merged via squash.

Prepared head SHA: 61fc06f33f
Co-authored-by: Ziy1-Tan <49604965+Ziy1-Tan@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-04-27 17:45:16 +08:00
Peter Steinberger
16eae4b4b4 fix(memory-core): skip cleanup after narrative fallback 2026-04-27 10:44:21 +01:00
Val Alexander
14a27e11f7 feat(ui): show raw config pending changes
Adds a raw config pending-changes diff panel in Control UI raw mode, with JSON5 parsing, sensitive-value redaction until explicit reveal, bounded diff work, and tests for redaction/reveal and stale reveal-state reset.

Also aligns provider manifest contract coverage for google-vertex and Qwen aliases to unblock the rebased CI matrix.

Supersedes stale PRs #48621 and #46654. PR #48621 had gone stale without maintainer follow-up, so this maintainer-authored PR carries the implementation forward transparently while preserving changelog credit for the original contributor and @BunsDev.
2026-04-27 04:42:10 -05:00
Peter Steinberger
531a0ddfe4 fix(config): repair retired llm timeout key 2026-04-27 10:39:56 +01:00
Vincent Koc
a50edbdc60 fix(cli): keep nodes list aligned with nodes status (#72619)
* fix(cli): keep nodes list aligned with nodes status

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

* fix(cli): keep nodes list aligned with nodes status
2026-04-27 02:39:33 -07:00
Vincent Koc
af03f9248d docs(feishu): clarify @all is not a bot mention (b642ebece9)
Trace to b642ebece9 (fix(feishu): do not treat @all as a bot mention).
Document the new behavior in the mention requirement section: broadcast-only
@all/@_all messages no longer wake the bot, while messages that combine @all
with a direct bot mention still count as a bot mention.
2026-04-27 02:38:52 -07:00
Peter Steinberger
733aaa0117 docs(cli): disambiguate migration import headings 2026-04-27 10:38:47 +01:00
Peter Steinberger
e862e0acb5 fix(providers): guard self-hosted model discovery 2026-04-27 10:38:17 +01:00
Peter Steinberger
f9b78fb08e docs(models): clarify local tool call workaround 2026-04-27 10:37:52 +01:00
Vincent Koc
59fb5fd3a7 fix(mattermost): prevent DM replies from creating threads (#72659)
* fix(mattermost): prevent DM replies from creating threads

* fix(mattermost): prevent DM replies from creating threads

* fix(mattermost): prevent DM replies from creating threads
2026-04-27 02:37:47 -07:00
Peter Steinberger
72f7d7e4ea fix(gateway): scope plugin subagent cleanup ownership 2026-04-27 10:36:33 +01:00
Vincent Koc
600df95c8c feat(migrate): add Claude importer
Add a bundled Claude migration provider for Claude Code and Claude Desktop imports.\n\nIncludes source discovery, preview/apply behavior for instructions, MCP servers, skills and command prompts, archive/manual handling for unsafe Claude state, docs, labeler, and tests.
2026-04-27 02:35:44 -07:00
Peter Steinberger
cf499101a2 fix(agents): normalize Windows runtime imports (#72731)
* fix(agents): normalize Windows runtime imports

* test(providers): align manifest contract coverage
2026-04-27 10:34:25 +01:00
Peter Steinberger
8b85f2c163 test: align provider contract aliases 2026-04-27 10:33:56 +01:00
Peter Steinberger
1ee885123f docs(models): document required tool choice workaround 2026-04-27 10:32:20 +01:00
Shakker
7d9dc8cf24 fix: reuse plugin manifests for model pricing refresh 2026-04-27 10:25:41 +01:00
Peter Steinberger
3af34316f2 fix: preserve clawhub install selectors 2026-04-27 10:25:21 +01:00
Peter Steinberger
1b81f75654 docs(providers): document cerebras setup 2026-04-27 10:22:21 +01:00
Peter Steinberger
4de235f908 feat(providers): add cerebras plugin 2026-04-27 10:22:20 +01:00
Peter Steinberger
08a002d8ab docs: document npm-only plugin installs 2026-04-27 10:20:30 +01:00
Peter Steinberger
13f9deb619 fix: audit windows task managed env drift 2026-04-27 10:19:50 +01:00
Peter Steinberger
cb9955dd5c fix: support npm-only plugin installs 2026-04-27 10:16:59 +01:00
Peter Steinberger
e899b32e1d fix(agents): collapse local model timeout knobs 2026-04-27 10:16:50 +01:00
Peter Steinberger
67f1266fe8 fix: repair managed service env install migration 2026-04-27 10:13:01 +01:00
Vincent Koc
b642ebece9 fix(feishu): do not treat @all as a bot mention (#72658)
* fix(feishu): do not treat @all as a bot mention

* fix(feishu): do not treat @all as a bot mention
2026-04-27 02:10:17 -07:00
Val Alexander
14ab00755f feat(ui): display agent identities in session list
Display friendly agent identity labels in the Control UI Sessions key column when identity data is available, keep raw-key fallback behavior, and allow filtering by agent identity name.

This is the maintainer-owned replacement for #54212 by @dingtao416. Thanks @dingtao416 for the original feature idea and implementation direction.

Includes follow-up fixes from maintainer review automation: normalized key-cell classes, own-property identity lookup, and friendly-label tooltips.

Validation:
- pnpm test ui/src/ui/format.test.ts ui/src/ui/views/sessions.test.ts
- pnpm check:changed

Closes #54163.
Supersedes #54212.
2026-04-27 04:09:39 -05:00
Peter Steinberger
9f450dcf06 fix: reject malformed clawhub plugin specs 2026-04-27 10:08:27 +01:00
Samuel Rodda
6c252cc54c fix(update): require applied gateway restarts
Require Control UI updates to observe a real gateway process replacement, surface skipped/error update outcomes, and verify the running gateway version after restart.\n\nAdds update.status restart-sentinel plumbing, docs, generated protocol model updates, and changelog attribution.\n\nLocal verification:\n- pnpm test src/gateway/server-methods/update.test.ts src/cli/gateway-cli/run-loop.test.ts src/infra/restart-sentinel.test.ts src/infra/process-respawn.test.ts src/infra/update-runner.test.ts ui/src/ui/app-gateway.node.test.ts ui/src/ui/controllers/config.test.ts\n- git diff --check\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/gateway/protocol.md docs/gateway/configuration.md docs/web/control-ui.md\n- pnpm docs:check-mdx
2026-04-27 04:07:43 -05:00
Peter Steinberger
b74f35ee6f refactor(plugins): move provider routing metadata to manifests 2026-04-27 10:06:30 +01:00
Peter Steinberger
57092a1794 ci: harden cross-os release harness on Windows 2026-04-27 10:03:38 +01:00
Peter Steinberger
3f895e5b49 test: dedupe hot unit fast coverage 2026-04-27 10:02:46 +01:00
Peter Steinberger
edbab0e2db fix: harden Google Live tool responses (#72426) (thanks @BsnizND) 2026-04-27 09:58:23 +01:00
BSnizND
409e762810 Fix Google Live tool response names 2026-04-27 09:58:23 +01:00
Peter Steinberger
b4b21cbc93 fix(browser): circuit-break managed launch failures 2026-04-27 09:58:14 +01:00
Vincent Koc
36a936af66 fix(update): add auto-update kill switch 2026-04-27 01:58:02 -07:00
Vincent Koc
caba05b94a fix(plugins): harden bundled install/uninstall sweep
Fix bundled plugin install/uninstall sweep coverage and avoid persisting invalid placeholder config for config-gated bundled plugins.
2026-04-27 01:57:40 -07:00
Peter Steinberger
7421112898 fix(agents): pass OpenAI SDK request timeouts 2026-04-27 09:55:39 +01:00
Peter Steinberger
cb45f16330 docs: clarify cron concurrency lanes 2026-04-27 09:54:58 +01:00
Peter Steinberger
04f76a8fdb test: remove duplicate plugin enable mock 2026-04-27 09:54:58 +01:00
Vincent Koc
b81eaf8a4e fix(agents): keep claude live streams valid 2026-04-27 01:53:37 -07:00
Peter Steinberger
6fddf17632 fix: accept clawhub plugin api wildcards 2026-04-27 09:48:01 +01:00
Peter Steinberger
6c8f0d04c3 test: trim unit-fast hotspots 2026-04-27 09:46:06 +01:00
Peter Steinberger
981cb89ea3 fix(agents): strip stale gemini assistant prefill 2026-04-27 09:41:37 +01:00
Peter Steinberger
a35ad200d1 test: shrink image sanitizer fixtures 2026-04-27 09:39:28 +01:00
Peter Steinberger
7d74c29dcc fix: isolate cron nested lane concurrency 2026-04-27 09:39:10 +01:00
Vincent Koc
231eb7b52a docs(migrating-hermes): note partial-apply guard introduced by 8bdfa58cbb
Trace to 8bdfa58cbb (fix(migrations): avoid partial Hermes config apply after
conflict). Hermes apply now marks remaining dependent config items as
"blocked by earlier apply conflict" when a conflict surfaces mid-apply,
instead of writing them partially. Document the user-visible reason string
and where to find blocked items in the migration report.
2026-04-27 01:38:49 -07:00
Peter Steinberger
f97cc58760 fix(browser): auto-start configured browser plugin 2026-04-27 09:37:10 +01:00
Shakker
e792f96a84 fix: cache capability provider manifest ids 2026-04-27 09:36:53 +01:00
Peter Steinberger
e21c909bd0 fix(agents): strip stale anthropic assistant prefill 2026-04-27 09:36:25 +01:00
Peter Steinberger
3be8e68898 test: dedupe fast lane imports 2026-04-27 09:35:41 +01:00
Vincent Koc
56ca4e2269 fix(daemon): handle sudo user-systemd gateway install failures
* fix(daemon): handle sudo user-systemd gateway install failures

* fix(daemon): harden sudo systemctl user scope

* fix(plugins): remove static type-cycle edges

* test(plugins): update bundle command config mock
2026-04-27 01:34:57 -07:00
Peter Steinberger
c25082f92e fix: apply cron concurrency to nested lane 2026-04-27 09:33:26 +01:00
Peter Steinberger
b9b15bec85 fix(ci): stabilize full validation probes 2026-04-27 09:30:53 +01:00
BsnizND
916eda16c1 fix(google-meet): keep tool sessions gateway-owned
Routes stateful Google Meet tool actions through the gateway-owned runtime so create/join/status/speak/leave share the same session owner instead of losing tool-created realtime sessions after the agent turn.

Also preserves structured gateway error details for missing session ids and tightens node-host child cleanup for already-closed sessions.

Fixes #72440.

Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
2026-04-27 09:28:14 +01:00
Peter Steinberger
b09afa2993 fix: keep auto model fallbacks pinned until reset 2026-04-27 09:27:19 +01:00
Peter Steinberger
a60f15c611 refactor(gateway): move model pricing policy to manifests 2026-04-27 09:26:53 +01:00
Vincent Koc
a494eea6d4 fix(gateway): defer hook request handler imports 2026-04-27 01:26:38 -07:00
Peter Steinberger
a95da5b52d fix(models): enrich local transport failure diagnostics 2026-04-27 09:25:38 +01:00
Peter Steinberger
c2d82b87ee test(plugins): mock registry contribution seam 2026-04-27 09:23:59 +01:00
Peter Steinberger
444acde1de feat: support layered plugin runtime deps 2026-04-27 09:21:25 +01:00
Peter Steinberger
9611260225 fix: retry primary after auto model fallback 2026-04-27 09:19:03 +01:00
Peter Steinberger
983bac7afa fix(plugins): keep registry lookup types acyclic 2026-04-27 09:16:43 +01:00
Peter Steinberger
3eb6a5b209 docs: format migration docs 2026-04-27 09:16:36 +01:00
Peter Steinberger
f9181835e8 fix(agents): warn on fake local tool calls 2026-04-27 09:14:59 +01:00
Shakker
51bd95fff3 fix: reuse extractor manifest resolution pass 2026-04-27 09:12:51 +01:00
Shakker
c60581740a fix: reuse manifest pass for runtime contract owners 2026-04-27 09:12:51 +01:00
Shakker
e547070ba9 fix: avoid repeated plugin metadata load for channel command defaults 2026-04-27 09:12:50 +01:00
Peter Steinberger
3913aa999d test: lighten fast lane imports 2026-04-27 09:12:17 +01:00
github-actions[bot]
b09345e3f6 chore(ui): refresh th control ui locale 2026-04-27 08:12:10 +00:00
Peter Steinberger
d76f924be3 fix(plugins): avoid registry barrel topology cycle 2026-04-27 09:09:31 +01:00
Peter Steinberger
5b616e2bec fix(agents): narrow session lock scope 2026-04-27 09:09:19 +01:00
Peter Steinberger
5ff49ae03e fix(gateway): skip local model pricing refreshes 2026-04-27 09:09:19 +01:00
bbddbb
563718c2e4 feat(control-ui): confirm dreaming restart changes
Require explicit confirmation before applying restart-impacting Dreaming mode changes in the Control UI.

- Add pending/confirm/loading state for the Dreaming toggle path
- Render a restart confirmation dialog before sending the config patch
- Sync Control UI locale metadata and cover the confirmation flow in browser tests

Fixes #63804
2026-04-27 03:08:59 -05:00
Peter Steinberger
276291d399 fix: hide bonjour Windows ARP shell probe 2026-04-27 09:08:40 +01:00
Peter Steinberger
8bdfa58cbb fix(migrations): avoid partial Hermes config apply after conflict 2026-04-27 09:07:59 +01:00
Vincent Koc
0055e404cf docs(hermes): rework CLI migrate page and add user-facing migration guide
- cli/migrate: convert flat reference into structured Mintlify page (Tip pointer, ParamField for flags, AccordionGroup for safety model, sub-sections for Hermes provider with what's imported, .env keys, archive-only state, and plugin contract)
- install/migrating-hermes: new dedicated user guide modeled after migrating-matrix.md (Tabs for onboarding vs CLI, AccordionGroup for what gets imported, Steps for recommended flow, Warning for --overwrite, Troubleshooting accordions)
- docs.json: add install/migrating-hermes to Maintenance group alongside migrating and migrating-matrix
2026-04-27 01:04:00 -07:00
Peter Steinberger
184b024fb6 test(migrate-hermes): keep config runtime stateful 2026-04-27 09:02:14 +01:00
Peter Steinberger
87b8072a85 test: cover qqbot channel guardrails 2026-04-27 08:58:13 +01:00
Peter Steinberger
f7081a3879 fix(lmstudio): trust configured local endpoints 2026-04-27 08:55:45 +01:00
Peter Steinberger
9510906669 fix: stop hook fallback after security blocks 2026-04-27 08:55:38 +01:00
Peter Steinberger
5a3d01e480 docs: format plugin sdk subpaths 2026-04-27 08:53:31 +01:00
Peter Steinberger
f21c8c3f0c test(migrate-hermes): use OpenClaw temp root 2026-04-27 08:53:27 +01:00
Peter Steinberger
58037cc89d fix: resolve browser playwright runtime deps 2026-04-27 08:50:56 +01:00
Peter Steinberger
c1d827844c test: speed up unit fast lane 2026-04-27 08:49:09 +01:00
Shakker
45b0d5ccc2 chore: add plugin lookup startup trace metrics 2026-04-27 08:48:18 +01:00
Shakker
bed76c26e7 fix: reuse lookup table for deferred plugin reload 2026-04-27 08:48:18 +01:00
Shakker
e068165036 docs: note plugin lookup reuse followups 2026-04-27 08:48:18 +01:00
Shakker
8b396bcfd2 docs: document plugin lookup table 2026-04-27 08:48:18 +01:00
Shakker
7c985890af refactor: reuse lookup table during gateway plugin load 2026-04-27 08:48:18 +01:00
Shakker
b2deb74694 fix: include setup cli backends in plugin lookup 2026-04-27 08:48:18 +01:00
Shakker
5228b24927 fix: avoid spread in provider owner lookup 2026-04-27 08:48:18 +01:00
Shakker
af29ccd98f fix: copy lookup startup plugin ids for gateway load 2026-04-27 08:48:18 +01:00
Shakker
f41126bc2e refactor: resolve contribution owners from lookup maps 2026-04-27 08:48:17 +01:00
Shakker
fbf0a29195 refactor: expand plugin lookup owner maps 2026-04-27 08:48:17 +01:00
Shakker
dc6ac472db refactor: use plugin lookup table for gateway load fallback 2026-04-27 08:48:17 +01:00
Shakker
123dee0513 fix: avoid duplicate plugin lookup diagnostics 2026-04-27 08:48:17 +01:00
Shakker
635af612d5 refactor: expose plugin lookup table normalizer 2026-04-27 08:48:17 +01:00
Shakker
354eb37ff5 refactor: reuse manifest registry for plugin id normalization 2026-04-27 08:48:17 +01:00
Shakker
b8c9426911 refactor: reuse plugin lookup table for contribution owners 2026-04-27 08:48:17 +01:00
Shakker
e985acbc1c docs: note plugin startup lookup table 2026-04-27 08:48:17 +01:00
Shakker
3f38d3af88 refactor: add plugin lookup table 2026-04-27 08:48:17 +01:00
Peter Steinberger
66f4b52db3 fix(docker): route local provider setup to host gateway 2026-04-27 08:46:33 +01:00
Alex Knight
4e19bc80c9 Fix null params for parameterless tools (#72673)
* fix tool null params for parameterless schemas

* guard composite required tool schemas
2026-04-27 17:45:59 +10:00
Vincent Koc
f4ca0612b2 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  docs: point maintainer triage at gitcrawl
  fix: clean runtime deps backup owner marker
  test(browser): close hanging attach-only sockets
  fix(plugins): normalize windows override imports
  fix: preserve live runtime deps temp dirs
  fix(lmstudio): promote bracketed tool calls
  Add Google Meet realtime consult agentId (#72381)
  fix: normalize lazy service override imports
  test: split ui unit tests from generic lane
  feat(migrations): add plugin-owned Hermes import
  fix(ci): expose package deps to Telegram QA harness (#72680)
  fix: hide bundled runtime npm windows
2026-04-27 00:44:30 -07:00
Vincent Koc
0286bb9817 docs: point maintainer triage at gitcrawl
Update the OpenClaw PR maintainer skill to use gitcrawl for local triage commands.
2026-04-27 00:43:07 -07:00
Peter Steinberger
84929bf85b fix: clean runtime deps backup owner marker 2026-04-27 08:43:03 +01:00
Peter Steinberger
bfdee5fa72 test(browser): close hanging attach-only sockets 2026-04-27 08:40:25 +01:00
Peter Steinberger
15e634d50c fix(plugins): normalize windows override imports 2026-04-27 08:39:42 +01:00
Peter Steinberger
4514a73170 fix: preserve live runtime deps temp dirs 2026-04-27 08:39:35 +01:00
Peter Steinberger
da55212c6e fix(lmstudio): promote bracketed tool calls 2026-04-27 08:38:53 +01:00
BsnizND
d5e6abcb3d Add Google Meet realtime consult agentId (#72381)
Remote proof:
- CI run 24982271745 passed on 6122e13c9f.
- Blacksmith Testbox tbx_01kq6vwehcszjfpp52f0pb3v1q passed focused Google Meet formatting, docs/link checks, realtime consult runtime tests, Google Meet tests, extension test typecheck, the core-unit-fast-support shard, and the core support boundary shard.

Thanks @BsnizND.

Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
2026-04-27 08:36:59 +01:00
Vincent Koc
29f4cdfcbb docs: point maintainer triage at gitcrawl 2026-04-27 00:36:32 -07:00
Peter Steinberger
f6db86f9a0 fix: normalize lazy service override imports 2026-04-27 08:35:45 +01:00
Peter Steinberger
98e7242b53 test: split ui unit tests from generic lane 2026-04-27 08:35:04 +01:00
Vincent Koc
1fc5b2b703 feat(migrations): add plugin-owned Hermes import
* feat: add migration providers

* feat: offer Hermes migration during onboarding

* feat(hermes): map imported config surfaces

* feat(onboard): require fresh migration imports

* docs(cli): clarify Hermes import coverage

* chore(migrations): rename Hermes importer package

* chore(migrations): rewire Hermes importer id

* fix(migrations): redact migration JSON details

* fix(hermes): use provider runtime for config imports

* test(hermes): cover missing source planning

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-27 00:34:29 -07:00
Vincent Koc
75c52b6c41 fix(ci): expose package deps to Telegram QA harness (#72680)
* fix(ci): expose package deps to telegram QA harness

* fix(ci): link QA package runtime deps

* fix(agents): guard replay metadata in empty retries

* fix(ci): keep plugin update smoke migration-stable
2026-04-27 00:33:29 -07:00
Peter Steinberger
d23ee2f702 fix: hide bundled runtime npm windows 2026-04-27 08:31:07 +01:00
Peter Steinberger
720ea766e6 fix(release): stabilize release validation probes 2026-04-27 08:27:45 +01:00
Vincent Koc
3200378ab4 fix(gateway): defer hook agent runner imports 2026-04-27 00:26:55 -07:00
Peter Steinberger
556c3e87df fix(agents): strip Gemma reasoning from local replay 2026-04-27 08:26:28 +01:00
Peter Steinberger
f427ddc220 fix(cli): keep update completion refresh lightweight 2026-04-27 08:24:26 +01:00
Vincent Koc
1ee893bc5f fix(gateway): defer http auth imports 2026-04-27 00:22:36 -07:00
Peter Steinberger
735890d955 fix(agents): dedupe subagent startup task 2026-04-27 08:20:45 +01:00
Peter Steinberger
daf8e14874 docs: fix msteams federated auth anchor 2026-04-27 08:20:20 +01:00
Peter Steinberger
708d833a76 test(ui): reuse ui test module environment 2026-04-27 08:19:24 +01:00
Peter Steinberger
646a268d27 fix: stage mirrored logger runtime deps 2026-04-27 08:17:18 +01:00
Peter Steinberger
729147dcb5 fix(cron): start isolated timeout after execution begins 2026-04-27 08:15:59 +01:00
Peter Steinberger
45778a840d fix(lmstudio): allow keyless local onboarding 2026-04-27 08:15:17 +01:00
Peter Steinberger
37cd6027cf test(gateway): harden session event setup timeout 2026-04-27 08:11:11 +01:00
Peter Steinberger
d1a8e2b17c docs: note vitest serial flag 2026-04-27 08:08:09 +01:00
Peter Steinberger
ec9b20263c fix(docker): expose QA harness exports for package Telegram 2026-04-27 08:07:27 +01:00
Vincent Koc
5333b1e2cc fix(gateway): defer channel runtime imports 2026-04-27 00:07:13 -07:00
Peter Steinberger
49ce7fe90c test: cover slack bolt auth verification suppression 2026-04-27 08:03:38 +01:00
Vincent Koc
baace37fef docs: sentence-case sweep across 10 more pages
- tools/plugin: Package Entrypoints
- tools/code-execution: How To Use It
- tools/browser-linux-troubleshooting: Root Cause, Config Reference
- install/bun: Lifecycle Scripts
- nodes/audio: Mention Detection in Groups
- nodes/images: Inbound Media to Commands (Pi)
- platforms/android: Connection Runbook
- plugins/building-plugins: Beta Release Testing
- web/control-ui: Content Security Policy
- security/THREAT-MODEL-ATLAS: Framework Attribution
2026-04-27 00:03:18 -07:00
Peter Steinberger
9d33da6ddf fix(agents): sanitize blank Bedrock user replay 2026-04-27 08:03:02 +01:00
Peter Steinberger
3d6d08116d fix(release): expose QA package exports in Telegram acceptance 2026-04-27 08:02:23 +01:00
Vincent Koc
2a17abcf5d docs: sentence-case sweep across 4 more pages
- platforms/digitalocean: Cost Comparison (2026), Oracle Cloud Free Alternative
- gateway/remote-gateway-readme: Quick Setup, How It Works
- cli/crestodian: Setup Bootstrap
- plugins/codex-harness: Computer Use
(Brand-named headings preserved across azure.md, web.md, firecrawl.md, clawhub.md, config-channels.md, security/index.md.)
2026-04-27 00:01:52 -07:00
Vincent Koc
6a03b76c9a docs: full-page sentence-case sweep across 6 pages
- platforms/oracle: Cost Comparison (2026), Still Recommended, Verify Security Posture
- install/exe-dev: Automated Install with Shelley, Remote Access
- platforms/mac/dev-setup: Build Fails: Toolchain or SDK Mismatch, App Crashes on Permission Grant
- reference/AGENTS.default: What OpenClaw Does, Core Skills, Usage Notes
- install/docker: Containerized Gateway, Agent Sandbox
- ci: Package Acceptance, Job Overview, Local Equivalents
2026-04-27 00:00:31 -07:00
Vincent Koc
89230f2480 fix(gateway): defer mcp loopback imports 2026-04-27 00:00:04 -07:00
Peter Steinberger
090063bd43 fix(ci): harden cron and Docker validation 2026-04-27 07:59:28 +01:00
Vincent Koc
41268ded2d docs: full-page sentence-case sweep across 5 worst-offender pages
- channels/msteams: 8 H2/H3 (Federated Authentication, Local Development, Known Limitations, Reply Style, Presentation Cards, Private Channels, etc.)
- auth-credential-semantics: 4 H2 (Stable Probe Reason Codes, Token Credentials, Explicit Auth Order Filtering, Probe Target Resolution)
- tools/browser: preserve brand-named headings (Browserless, WebSocket CDP, Chrome MCP, Control API, Brave); minor cleanup
- security/CONTRIBUTING-THREAT-MODEL: 4 H2/H3 (What We Use, Risk Levels, Review Process; Threat IDs preserved as branded label)
- gateway/multiple-gateways: 4 H2 (Best Recommended Setup, Why This Works, General Multi-Gateway Setup, Isolation Checklist)
2026-04-26 23:58:35 -07:00
Peter Steinberger
f89d0f7c53 fix(cron): preserve telegram direct thread inference 2026-04-27 07:58:03 +01:00
Vincent Koc
ca9a04b271 docs: full-page readability pass on 5 worst-offender pages
- automation/standing-orders: sentence-case all H2/H3 headings (Why Standing Orders, Anatomy, Execute-Verify-Report Pattern, Multi-Program Architecture, Best Practices, etc.) and clean up Related link styling
- platforms/raspberry-pi: sentence-case 10 headings (Hardware Requirements, Performance Optimizations, Cost Comparison, etc.)
- install/fly: sentence-case troubleshooting and Private Deployment headings
- pi-dev: drop 'this guide summarizes' filler, sentence-case 4 H2 headings, restore brand-cased Pi
- concepts/model-providers: sentence-case Kimi Coding (other brand-named providers preserved as-is: Google Gemini, Google Vertex, Kilo Gateway, Volcano Engine)
2026-04-26 23:57:10 -07:00
Peter Steinberger
37d37d3779 fix(cron): tolerate legacy flat schedule identity 2026-04-27 07:56:26 +01:00
Peter Steinberger
53f536b368 fix: avoid slack startup auth rejection leak 2026-04-27 07:55:57 +01:00
Peter Steinberger
725938f0f5 test: avoid heavy registry imports in web provider tests 2026-04-27 07:53:09 +01:00
Vincent Koc
d43bf6de0a docs: batch sentence-case headings across high-Title-Case offenders
- pi.md: 9 H2 + 14 H3 (Package Dependencies, File Structure, Tool Pipeline, etc.)
- cli/hooks.md: 6 H2 (List All Hooks, Get Hook Information, etc.)
- plugins/message-presentation.md: 8 H2 (Producer Examples, Renderer Contract, etc.)
- plan/ui-channels.md: 7 H2 (Non Goals, Target Model, Refactor Steps, etc.)
- install/ansible.md: 6 H2 + 1 H3 (What You Get, Quick Start, etc.)

Mintlify anchor generation prefers sentence case for predictable URLs.
2026-04-26 23:52:28 -07:00
Peter Steinberger
18b6c3bb61 fix(docker): recognize current gateway readiness logs 2026-04-27 07:51:59 +01:00
Vincent Koc
4a30ae182b fix(gateway): defer embedded runner imports 2026-04-26 23:51:08 -07:00
Vincent Koc
69c30e37d9 fix(memory-lancedb): skip processed auto-capture messages safely (#72663) 2026-04-26 23:51:04 -07:00
Peter Steinberger
9ced682a9d fix(cron): omit disabled delivery trace errors 2026-04-27 07:50:50 +01:00
Peter Steinberger
4db1faaafc chore: install discord clawd skill 2026-04-27 07:50:15 +01:00
Peter Steinberger
c754370100 docs: document maintainer testbox opt-in 2026-04-27 07:49:28 +01:00
BsnizND
2785be2604 Fix Google Meet realtime interruption playback (#72524)
Fixes #72523.

Remote proof:
- CI run 24980529154 passed on 29f825bea5.
- Blacksmith Testbox tbx_01kq6tsgbaxgstxmtearwy9n4w passed focused formatting, Google Meet tests, Google realtime provider tests, and extension test typecheck.

Thanks @BsnizND.

Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
2026-04-27 07:49:10 +01:00
Peter Steinberger
8811112ab3 fix(release): stabilize full validation lanes 2026-04-27 07:46:44 +01:00
Peter Steinberger
ddcd9d62c4 fix(cron): invalidate stale external schedule slots 2026-04-27 07:46:08 +01:00
Peter Steinberger
3173842913 fix: keep staged plugin mirrors idempotent 2026-04-27 07:44:15 +01:00
Peter Steinberger
566295cd34 fix: materialize stale runtime mirror symlinks 2026-04-27 07:42:47 +01:00
Peter Steinberger
04be516926 fix(gateway): keep liveness probes independent of config load 2026-04-27 07:42:14 +01:00
Peter Steinberger
7559845597 fix(ollama): avoid implicit native num_ctx override 2026-04-27 07:42:14 +01:00
Vincent Koc
c4194b8345 docs(voice-call): note SecretRef support for twilio.authToken and tts.providers.*.apiKey
Trace to db09f68ce5 (Support SecretRef for voice-call credentials and bundled
plugin SecretInputs #72607). The reference page docs/reference/secretref-credential-surface.md
listed the new entries in the same SHA, but docs/plugins/voice-call.md showed
only plain-string credentials without pointing to the SecretRef surface.
2026-04-26 23:39:51 -07:00
Vincent Koc
015f7dc747 fix(agents): refresh bootstrap snapshot when workspace files change (#72406)
* fix(agents): refresh bootstrap snapshot when workspace files change

* fix(clownfish): address review for ghcrawl-207042-agentic-merge (1)
2026-04-26 23:39:33 -07:00
Peter Steinberger
c110f8c028 fix(docker): stabilize bundled channel release lanes 2026-04-27 07:37:28 +01:00
BsnizND
f2a17b2991 Fix Google Meet chrome-node bridge cleanup (#72372)
Fixes #72371.

Remote proof:
- CI run 24980121791 passed on d583a6b615.
- Blacksmith Testbox tbx_01kq6t5jk2f51gxq30j9veyjhy passed focused Google Meet formatting and tests.

Thanks @BsnizND.

Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
2026-04-27 07:37:18 +01:00
Vincent Koc
5c591a4e13 fix(test): build missing Docker images in Testbox 2026-04-26 23:33:43 -07:00
Peter Steinberger
67e6410e0f ci: accept legacy bundled docker lane 2026-04-27 07:31:18 +01:00
Peter Steinberger
4bca42d933 fix(cron): alert on persistent skipped runs 2026-04-27 07:31:04 +01:00
Vincent Koc
b246c06fa5 fix(daemon): surface systemd user-bus hints during gateway install (#72617) 2026-04-26 23:30:54 -07:00
Vincent Koc
dcff28d285 fix(telegram): hide acknowledged failed-tool warnings from chat (#72410)
* fix(telegram): hide acknowledged failed-tool warnings from chat

* fix(clownfish): address review for ghcrawl-207034-agentic-merge (1)

* fix(clownfish): address review for ghcrawl-207034-agentic-merge (1)
2026-04-26 23:29:19 -07:00
Peter Steinberger
ca44ab65e6 ci(release): allow live E2E actions reads 2026-04-27 07:26:33 +01:00
Peter Steinberger
9313554a8d test: stabilize matrix block streaming prompt 2026-04-27 07:25:52 +01:00
Peter Steinberger
edf43dfc88 ci: fix update channel package version probe 2026-04-27 07:23:23 +01:00
Peter Steinberger
cf04fa24d8 test(openai): prefer exact live registry models 2026-04-27 07:22:26 +01:00
Bek
aac83e00cf fix: Slack inbound thread session routing (#72498)
Normalize actionable Slack thread roots and follow-up replies onto the same thread parent session key.
2026-04-27 02:19:27 -04:00
Peter Steinberger
93ac2cefaa ci(docker): resolve short refs before checkout 2026-04-27 07:18:57 +01:00
Peter Steinberger
a3fcb8db79 ci(docker): split bundled release lanes 2026-04-27 07:17:14 +01:00
Josh Avant
db09f68ce5 Support SecretRef for voice-call credentials and bundled plugin SecretInputs (#72607)
* fix: support voice-call secretrefs

* test: classify plugin secretref targets

* docs: credit voice-call secretref change

* fix: keep plugin secret target discovery lightweight
2026-04-27 01:16:50 -05:00
Vincent Koc
ab237fe7b0 fix(gateway): defer chat startup helpers 2026-04-26 23:15:26 -07:00
Peter Steinberger
1dac448ff0 fix: wait for qa gateway restart boundary 2026-04-27 07:13:39 +01:00
Vincent Koc
1427c3a78d fix(sessions_spawn): tolerate ACP-only fields for subagent runtime (#72331) 2026-04-26 23:11:42 -07:00
Peter Steinberger
44a504cd39 ci: time-box package acceptance legacy compat 2026-04-27 07:11:14 +01:00
Vincent Koc
e6d2c9b080 fix(process): decode Windows command output with console codepage awareness (#72393)
* fix(process): decode Windows command output with console codepage awareness

* fix(clownfish): address review for ghcrawl-199248-agentic-merge (1)
2026-04-26 23:10:59 -07:00
Peter Steinberger
5cc06c69a9 fix(discord): preserve explicit delivery target kind 2026-04-27 07:09:45 +01:00
Peter Steinberger
ca67762b88 fix(image): honor media timeouts 2026-04-27 07:09:36 +01:00
Peter Steinberger
19cb9ca6bf fix: materialize staged plugin runtime chunks 2026-04-27 07:08:44 +01:00
Vincent Koc
8440f67935 fix(gateway): defer chat event imports 2026-04-26 23:07:05 -07:00
Peter Steinberger
6175309c01 fix: normalize openai legacy image sizes 2026-04-27 07:05:56 +01:00
Vincent Koc
b1812387a0 fix(agent): harden empty attempt retry handling 2026-04-26 23:04:40 -07:00
Josh Avant
b3d9948c4c fix: use runtime snapshot for TTS SecretRefs (#72581)
* fix: use runtime snapshot for tts secrets

* fix: keep tts secret snapshot selection local

* docs: add tts secretref changelog entry
2026-04-27 01:02:17 -05:00
Peter Steinberger
ac5a1d1622 ci: forward package acceptance live secrets 2026-04-27 07:00:11 +01:00
Peter Steinberger
2a6fab9d22 docs: point release evidence at public checks 2026-04-27 06:57:47 +01:00
Vincent Koc
c7d77f8c7b fix(gateway): defer plugin HTTP dispatch 2026-04-26 22:55:26 -07:00
Peter Steinberger
32aa631e19 test: relax matrix block streaming qa timeout 2026-04-27 06:54:43 +01:00
Peter Steinberger
8de02c318b fix: reclaim orphan session write locks 2026-04-27 06:54:43 +01:00
Peter Steinberger
e962381dbf ci: fix plugin update smoke quoting 2026-04-27 06:50:59 +01:00
Vincent Koc
b02cca4e00 fix(gateway): trim startup imports 2026-04-26 22:48:31 -07:00
Alex Knight
06b3e4ef8a Fail invalid plugin registration gates loudly (#72577)
* fix plugin registration gate failures
2026-04-27 15:46:50 +10:00
Peter Steinberger
85148f3b20 refactor(cron): split notification routing 2026-04-27 06:44:53 +01:00
Peter Steinberger
4b9c85776d ci: allow package plugin metadata migrations 2026-04-27 06:42:14 +01:00
Vincent Koc
6bbb1b79e1 fix(doctor): treat gateway memory probe timeout as inconclusive (#72618) 2026-04-26 22:40:26 -07:00
Peter Steinberger
45bdfb5f72 ci(docker): keep release path at three chunks 2026-04-27 06:39:46 +01:00
Vincent Koc
60d4d5e1fa fix(daemon): reconcile macOS LaunchAgent supervision state (#72616) 2026-04-26 22:39:15 -07:00
Peter Steinberger
8c2f894d3a docs(ollama): expand setup recipes 2026-04-27 06:37:49 +01:00
Josh Avant
510718bedf fix(runtime): resolve web search SecretRefs from snapshots (#72563) 2026-04-27 00:35:21 -05:00
Peter Steinberger
332cdd7aca fix(cron): route failure alerts via target session 2026-04-27 06:34:38 +01:00
Peter Steinberger
422fa99197 fix(models): honor provider context defaults 2026-04-27 06:32:24 +01:00
Peter Steinberger
5e9a96fafb ci(docker): reuse cached e2e images for reruns 2026-04-27 06:29:09 +01:00
Peter Steinberger
679e476183 ci: always shard full Matrix QA 2026-04-27 06:28:35 +01:00
Vincent Koc
3d59e8192b fix(cli): restore help registration and descriptor graph 2026-04-26 22:26:59 -07:00
Peter Steinberger
02dae3e1d1 ci: fix telegram package acceptance harness 2026-04-27 06:26:44 +01:00
Peter Steinberger
835c6bc0c1 ci: tolerate legacy package acceptance metadata 2026-04-27 06:26:08 +01:00
Peter Steinberger
52249927ac fix(ollama): skip localhost discovery for remote providers 2026-04-27 06:24:43 +01:00
Peter Steinberger
b94ad7c9d8 fix(ollama): retry non-visible reasoning turns 2026-04-27 06:19:22 +01:00
Peter Steinberger
32b1f0ce74 ci: narrow package acceptance to artifact lanes 2026-04-27 06:17:05 +01:00
Peter Steinberger
1ea12fe3e2 fix: stage bundled plugin runtime deps safely 2026-04-27 06:16:26 +01:00
Vincent Koc
6038725501 docs: batch convert remaining prose callouts to Mintlify components
- platforms/android: blockquote Note for Android app status, Note for canvas host port
- platforms/macos: Tip component for app vs CLI discovery comparison
- plugins/zalouser, channels/zalouser: blockquote Warning components for unofficial automation risk
- channels/pairing: convert two Important paragraphs to Note components for DM-vs-group scope and silent-upgrade behavior
2026-04-26 22:15:11 -07:00
Vincent Koc
a108169127 fix(gateway): lazy-load setup wizard runtime 2026-04-26 22:12:46 -07:00
Vincent Koc
5bba899a70 docs: batch fix filler Note/page openers and one TUI auth Warning
- gateway/authentication: tighten model-provider Note opener
- help/debugging: drop 'this page covers' filler
- reference/session-management-compaction: rephrase end-to-end intro
- reference/transcript-hygiene: drop 'this document describes' filler
- web/index: collapse 'this page focuses' filler
- web/tui: convert prose --url Note to Warning component
2026-04-26 22:12:17 -07:00
Vincent Koc
9df7fe3986 docs: fix live docs callout formatting 2026-04-26 22:08:22 -07:00
Vincent Koc
5c3e2a6b44 docs: batch fix filler openings across providers, platforms, install, tools, and pi
- platforms/mac/dev-setup: sentence-case heading and direct opener
- tools/browser-wsl2-windows-remote-cdp-troubleshooting: collapse three-bullet split-host setup into one direct sentence
- install/migrating-matrix: drop 'this page covers' filler
- providers/perplexity-provider: rephrase Note opener
- pi: drop 'this document describes' filler
2026-04-26 22:07:47 -07:00
Vincent Koc
51dbda3f3d docs(automation+start): batch fix filler openings and prose Tip
- start/openclaw: workspace-as-memory Tip component
- automation/tasks: drop 'this page covers' filler in Note
- automation/auth-monitoring, clawflow, cron-vs-heartbeat: collapse 'this page moved... See X' redirects to single direct sentences
2026-04-26 22:04:56 -07:00
Peter Steinberger
488a1ee146 fix(cron): preserve silent tool results 2026-04-27 06:04:27 +01:00
Vincent Koc
a167e687ce docs: fix live docs CI 2026-04-26 22:04:16 -07:00
Peter Steinberger
2dcc4605d4 fix(llm-task): normalize provider-prefixed model overrides 2026-04-27 06:02:16 +01:00
Vincent Koc
05ebfa4146 docs(help+tools): batch convert prose callouts to Mintlify components
- testing-live: Tip components for model-discovery and authoritative-list guidance
- debugging: --dev flag Note and non-dev gateway stop Tip
- testing: narrowing live tests Tip
- tools/lobster: optional-plugin allowlist Note
- tools/acp-agents-setup: blockquote Important to Warning component
2026-04-26 22:01:55 -07:00
Peter Steinberger
86da88c120 ci: request release evidence after full validation 2026-04-27 06:01:06 +01:00
Vincent Koc
9624d81bb3 docs(install): batch convert callouts and sentence-case headings
- macos-vm: download-time Note component
- hetzner: community-maintained Note component
- exe-dev: stateful-VM Tip component
- development-channels: parallel clones Tip component
- migrating: sentence-case top heading and section headings, replace bullet -- separators with em-dashes, drop 'this guide' filler
2026-04-26 21:59:42 -07:00
Peter Steinberger
751c7f32a5 fix(cli): preserve Matrix QA profile flag 2026-04-27 05:57:37 +01:00
Vincent Koc
6c49039a23 docs(gateway): batch convert callouts and fix JSON5 smart quotes
- security/index: 3 prose callouts (Note/Warning) for remote credential rules, sandbox scope, elevated mode
- tailscale: loopback Note component
- pairing: bulleted Important warning to Warning component
- openshell: host-edit warning to Warning component
- local-models: replace 13 smart quotes inside the LM Studio JSON5 example so it parses
2026-04-26 21:56:59 -07:00
Vincent Koc
91e835ebe0 docs(concepts): batch readability and Mintlify component pass
- memory: replace en-dash list separators with em-dashes, sentence-case Further reading link titles
- messages: rewrite filler 'this page ties together' opener to a direct one
- delegate-architecture: convert 4 blockquote security warnings to Warning and Note components
- system-prompt: convert blockquote daily-memory note to Note component
2026-04-26 21:54:23 -07:00
Peter Steinberger
5d5c37775e fix(ollama): estimate usage when counters are omitted 2026-04-27 05:54:03 +01:00
Peter Steinberger
377553e41a ci: link package deps for telegram acceptance 2026-04-27 05:52:13 +01:00
Gustavo Madeira Santana
241d0cb88e chore(docs): dedupe and simplify matrix docs 2026-04-27 00:52:04 -04:00
Vincent Koc
dc8b881c11 fix(gateway): defer startup runtime imports 2026-04-26 21:50:50 -07:00
Vincent Koc
f4129cdd2b docs(channels): batch convert prose callouts to Mintlify components
- msteams: 5 callouts (Note/Warning) for preview status, devtunnel auth, group policy, multi-tenant deprecation, user-prefix targeting
- slack: replyToMode threading note
- whatsapp: dms vs direct prompt override note
- group-messages: mentionPatterns cross-channel note
- signal: signal-cli main session de-auth warning
2026-04-26 21:49:56 -07:00
Vincent Koc
6908bd3167 docs(cli): batch readability pass for 5 CLI pages
- channels: convert Tip prose to component, fix /channels/index link, sentence-case heading
- configure: convert Note and Tip prose to components
- devices: convert Note and Warning prose to components
- models: sentence-case scan/status subheadings
- agents: clean up related links and Title Case body link
2026-04-26 21:47:29 -07:00
Peter Steinberger
7564af24e6 fix(providers): preserve configured model input modalities 2026-04-27 05:46:53 +01:00
Peter Steinberger
748daa4857 ci: make package acceptance legacy-safe 2026-04-27 05:46:06 +01:00
Peter Steinberger
6987132aed ci: add Matrix QA profiles 2026-04-27 05:43:14 +01:00
Peter Steinberger
382e03a2d8 fix(cron): fail isolated runs on run-level errors 2026-04-27 05:42:59 +01:00
Peter Steinberger
390b965460 docs: document release evidence workflow 2026-04-27 05:40:21 +01:00
Vincent Koc
edbcfe1a1d docs(agents): keep testbox policy out of root rules 2026-04-26 21:39:23 -07:00
Vincent Koc
e2ecf292bc docs(doctor): document models.providers.api migration and stale-enum skip
Add the legacy `models.providers.*.api: "openai"` → `"openai-completions"`
migration to doctor's Current migrations list, and note the gateway startup
behavior that skips providers with future or unknown api enum values instead
of failing closed.

Traces to:
- 6a7980e984 fix(doctor): migrate legacy OpenAI provider api
- 147f4f50f5 fix(gateway): skip stale model provider api entries
2026-04-26 21:39:00 -07:00
Peter Steinberger
fd06aeac04 test(docker): fixture ClawHub plugin smoke 2026-04-27 05:38:27 +01:00
Vincent Koc
f83e424a5d docs: fix onboarding docs formatting 2026-04-26 21:33:58 -07:00
Vincent Koc
0eac6432c3 docs: fix docs formatting drift 2026-04-26 21:29:38 -07:00
Vincent Koc
ebbc7dcfeb docs(updating): group advanced npm topics in AccordionGroup 2026-04-26 21:29:03 -07:00
Vincent Koc
8cd68487d9 docs(remote): rename numbered headings and use Note components 2026-04-26 21:29:03 -07:00
Vincent Koc
4519b29419 docs(update): convert flow steps to Steps component 2026-04-26 21:29:02 -07:00
Vincent Koc
c881d8da48 docs(sandbox): replace bold-callout patterns with Note and Tip components 2026-04-26 21:29:02 -07:00
Vincent Koc
00300b85d0 docs(onboard): convert related-guides to CardGroup and group flow notes 2026-04-26 21:29:01 -07:00
Peter Steinberger
7c0fdae9b9 docs(providers): document local model request timeout 2026-04-27 05:27:41 +01:00
Gustavo Madeira Santana
e0956a0853 fix(cli): skip startup work for positional help 2026-04-27 00:24:06 -04:00
Vincent Koc
9c07579a95 docs(testbox): align maintainer testbox mode 2026-04-26 21:23:28 -07:00
Vincent Koc
166a6d9088 docs(feishu): convert blockquote callouts to Note components 2026-04-26 21:22:58 -07:00
Vincent Koc
5a88d8502f docs(gateway): split lifecycle notes accordion 2026-04-26 21:22:57 -07:00
Vincent Koc
4db066d102 docs(ollama): restructure auth rules and fix duplicate card titles 2026-04-26 21:22:57 -07:00
Vincent Koc
3f1ce689a1 docs(compaction): dedupe sections and consolidate config 2026-04-26 21:22:57 -07:00
Vincent Koc
d4bb4912fc docs(cron): regroup notes into themed sections 2026-04-26 21:22:56 -07:00
Peter Steinberger
02455c0c52 ci: include telegram in release package acceptance 2026-04-27 05:14:19 +01:00
Peter Steinberger
d857989111 docs: clarify package acceptance release role 2026-04-27 05:13:41 +01:00
Vincent Koc
4c3c3abe1a fix(cli): keep startup help metadata on fast path 2026-04-26 21:11:23 -07:00
Vincent Koc
716b3faf7e Revert "docs(agents): document testbox maintainer workflow"
This reverts commit 4340cb74c2.
2026-04-26 21:10:09 -07:00
Vincent Koc
3e95927df7 Merge branches 'main' and 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  docs: explain telegram package artifact testing
  ci: let telegram e2e use package artifacts
  docs: explain release validation entrypoints
  ci: tolerate legacy qa inventory entries
  ci(testbox): save build artifact cache before wait
  fix: allow heavyweight docker lanes at low parallelism
  test(docker): use packaged gateway expect-final smoke
  test(live): accept current Codex status text

* 'main' of https://github.com/openclaw/openclaw:
  docs: explain telegram package artifact testing
  ci: let telegram e2e use package artifacts
  docs: explain release validation entrypoints
  ci: tolerate legacy qa inventory entries
  ci(testbox): save build artifact cache before wait
  fix: allow heavyweight docker lanes at low parallelism
  test(docker): use packaged gateway expect-final smoke
  test(live): accept current Codex status text
2026-04-26 21:09:46 -07:00
Peter Steinberger
cc79f4982c docs: explain telegram package artifact testing 2026-04-27 05:09:17 +01:00
Peter Steinberger
09107e0b7f ci: let telegram e2e use package artifacts 2026-04-27 05:09:16 +01:00
Peter Steinberger
720ab99307 docs: explain release validation entrypoints 2026-04-27 05:07:22 +01:00
Peter Steinberger
0ff0c7ce57 ci: tolerate legacy qa inventory entries 2026-04-27 05:07:15 +01:00
Vincent Koc
a33a2c97a3 ci(testbox): save build artifact cache before wait 2026-04-26 21:07:02 -07:00
Vincent Koc
4cc572a813 ci(testbox): save build artifact cache before wait 2026-04-26 21:06:29 -07:00
Peter Steinberger
3c8760f16d fix: allow heavyweight docker lanes at low parallelism 2026-04-27 05:04:52 +01:00
Peter Steinberger
940f67e524 test(docker): use packaged gateway expect-final smoke 2026-04-27 05:01:36 +01:00
Vincent Koc
ef828d55af test(live): accept current Codex status text
Accept current Codex harness status prose while still requiring the OpenClaw status shape, active model, and live harness session.
2026-04-26 21:01:22 -07:00
Vincent Koc
9626ef274a ci(testbox): add build artifact cache warmup 2026-04-26 20:58:14 -07:00
Val Alexander
5e8cb77e79 Polish Control UI quick settings layout
Polish the Control UI quick settings dashboard layout.

- Rework quick settings into a 12-column desktop grid with matched top-row card heights.
- Pair Personal with a right-side Appearance/Automations stack on large screens while preserving tablet/mobile ordering.
- Add render/style guards plus an Unreleased changelog entry crediting @BunsDev.

Validated with focused UI tests, formatting, git diff checks, local changed gate, and full PR CI.
2026-04-26 22:56:35 -05:00
Val Alexander
461c10bb51 feat(onboard): support non-interactive GitHub Copilot token auth
Add manifest-owned GitHub Copilot token support for non-interactive onboarding, including documented env fallback, ref-mode tokenRef storage, saved-profile reuse, and default model wiring that preserves existing primary model configuration.

Validation:
- pnpm test extensions/github-copilot/index.test.ts src/plugins/contracts/registry.contract.test.ts src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts
- pnpm check:changed
- CI green on aadac2c8d4
2026-04-26 22:56:20 -05:00
Peter Steinberger
18b76e3995 fix(ollama): scope request timeouts to providers 2026-04-27 04:55:11 +01:00
joshavant
6b6f8ab1aa Revert "fix: resolve tts secret refs for local infer (#72549)"
This reverts commit 4878d3e059.
2026-04-26 22:54:08 -05:00
Peter Steinberger
36c08e0288 test(docker): keep web search smoke on one gateway connection 2026-04-27 04:51:55 +01:00
Peter Steinberger
6590e0e872 docs: expand release validation runbook 2026-04-27 04:50:51 +01:00
Vincent Koc
4340cb74c2 docs(agents): document testbox maintainer workflow 2026-04-26 20:49:56 -07:00
Peter Steinberger
5f9506f7fd ci: avoid inherited package acceptance secrets 2026-04-27 04:44:29 +01:00
Gustavo Madeira Santana
e1cdaa3c88 docs(matrix): note E2EE setup improvements 2026-04-26 23:42:32 -04:00
Gustavo Madeira Santana
2b40416314 test(matrix): speed up CLI metadata entry test 2026-04-26 23:40:53 -04:00
Gustavo Madeira Santana
3b74b913e3 fix(matrix): avoid device cleanup sync races 2026-04-26 23:40:52 -04:00
Gustavo Madeira Santana
99159f89da fix(matrix): stabilize e2ee qa flows 2026-04-26 23:40:52 -04:00
Peter Steinberger
02d266c6c4 ci: split package acceptance refs 2026-04-27 04:39:19 +01:00
Ayaan Zaidi
34f81c6a8a docs(changelog): note model provider api recovery 2026-04-27 09:07:31 +05:30
Ayaan Zaidi
147f4f50f5 fix(gateway): skip stale model provider api entries 2026-04-27 09:07:31 +05:30
Ayaan Zaidi
6a7980e984 fix(doctor): migrate legacy OpenAI provider api 2026-04-27 09:07:31 +05:30
Vincent Koc
831f03b814 fix(cli): speed up gateway status config reads 2026-04-26 20:34:49 -07:00
Peter Steinberger
b0c70786fd fix(cron): preserve structured denial failures 2026-04-27 04:34:38 +01:00
Peter Steinberger
e6eea6cfe2 docs: clarify package acceptance npm selection 2026-04-27 04:34:13 +01:00
Peter Steinberger
67650c4c0a fix(ollama): resolve custom local provider auth 2026-04-27 04:33:18 +01:00
Vincent Koc
f60378519c test(plugins): cover bundled dependency edge cases 2026-04-26 20:31:54 -07:00
Josh Avant
4878d3e059 fix: resolve tts secret refs for local infer (#72549) 2026-04-26 22:31:39 -05:00
Peter Steinberger
6a05b9eec5 ci: fix package acceptance permissions 2026-04-27 04:27:45 +01:00
Peter Steinberger
2c092a0eff docs: document release validation test workflows 2026-04-27 04:27:07 +01:00
Peter Steinberger
76de167ca1 ci: add package acceptance workflow 2026-04-27 04:25:31 +01:00
jnuyao
2a08848dd1 feat(feishu): display group names in session labels
Resolve Feishu group chat labels through getChatInfo so session labels prefer human-readable group names over raw chat IDs.\n\nPreserve topic/thread label priority and defer the lookup until after broadcast dedup claims to avoid duplicate account API calls.\n\nValidation:\n- pnpm test extensions/feishu/src/bot-group-name.test.ts extensions/feishu/src/bot.broadcast.test.ts\n- pnpm check:changed\n- GitHub CI green on c154dc0a41fd715dce95ef1fb5d0c269533b8c22\n\nCloses #35675
2026-04-26 22:22:51 -05:00
Peter Steinberger
d3fd275aa5 test: cover gateway wrapper persistence in docker e2e 2026-04-27 04:15:33 +01:00
Peter Steinberger
6c1cffa7f8 ci: fix targeted live model provider run 2026-04-27 04:08:16 +01:00
Peter Steinberger
e0141946b2 ci: allow targeted live model providers 2026-04-27 04:04:38 +01:00
Peter Steinberger
cbbd860ef9 test(docker): isolate installer smoke sessions 2026-04-27 04:01:46 +01:00
Peter Steinberger
9bd4200f3c docs: prefer targeted test reruns 2026-04-27 04:00:05 +01:00
Peter Steinberger
a72522d05d test: prefer glm 5 in live sweeps 2026-04-27 03:56:16 +01:00
Peter Steinberger
313a19c940 fix(ollama): scope auth to local hosts 2026-04-27 03:54:12 +01:00
Peter Steinberger
29af4add2a feat: trigger compaction for oversized transcripts 2026-04-27 03:46:11 +01:00
Vincent Koc
d5063d5b16 fix(telegram): avoid materializing tool-progress drafts
Address Clownfish follow-up on Telegram native draft finalization. Requires real streamed assistant partials before materializing drafts, clears stale native draft previews, and keeps media/buttons on normal send path.
2026-04-26 19:43:23 -07:00
Peter Steinberger
6d0e84aadb test(docker): skip bootstrap ritual in install smoke 2026-04-27 03:41:47 +01:00
Peter Steinberger
ef31a333f7 docs: add gateway wrapper install examples 2026-04-27 03:40:32 +01:00
Peter Steinberger
0b3f13b337 fix: preserve wrapper env during gateway reinstall 2026-04-27 03:40:32 +01:00
Peter Steinberger
9f9bd41f40 fix: persist gateway service wrappers 2026-04-27 03:40:32 +01:00
Peter Steinberger
414fd41a1f fix(ollama): avoid timing out active model pulls 2026-04-27 03:40:28 +01:00
Peter Steinberger
8b27c489f5 test: bound openai websocket live e2e 2026-04-27 03:39:24 +01:00
Vincent Koc
f39f4629d9 docs(changelog): credit update fixture repair
Add the missing Unreleased changelog credit for the Docker update-channel fixture repair.
2026-04-26 19:38:07 -07:00
Peter Steinberger
348728c28c fix(providers): bound native fetch timeouts 2026-04-27 03:33:51 +01:00
Peter Steinberger
dc78d58448 fix(ollama): honor baseURL provider aliases 2026-04-27 03:28:23 +01:00
Vincent Koc
ae89d44760 chore(plugin-sdk): refresh api baseline 2026-04-26 19:24:37 -07:00
Vincent Koc
ead76f61d8 fix(cli): skip plugin preload for plugin updates 2026-04-26 19:24:37 -07:00
Vincent Koc
a5f6603e61 fix(release): clarify control ui build requirement 2026-04-26 19:24:37 -07:00
Vincent Koc
a313c4db92 chore(config): refresh bundled channel metadata 2026-04-26 19:24:36 -07:00
Peter Steinberger
b72c0bdfad ci: force gemini api key auth in acp bind 2026-04-27 03:23:00 +01:00
Peter Steinberger
bd42f35097 fix(ui): show configured thinking defaults 2026-04-27 03:21:49 +01:00
Peter Steinberger
90ad79cbcd test(docker): generate update fixture ui asset 2026-04-27 03:13:51 +01:00
Peter Steinberger
0b46227d6c fix(ollama): keep configured max thinking compatible 2026-04-27 03:13:15 +01:00
Peter Steinberger
1882a8e5ea fix: refresh preflight rotated runs 2026-04-27 03:12:45 +01:00
Vincent Koc
f5f4f514d8 docs(changelog): backfill gateway memory fixes 2026-04-26 19:11:13 -07:00
Vincent Koc
0c30d0d0b8 fix(gateway): resolve configured thinking default in session rows (#72324)
* fix(gateway): resolve configured thinking default in session rows

* fix(gateway): preserve model thinking precedence
2026-04-26 19:10:21 -07:00
Peter Steinberger
de0ece20d1 test: accept live release validation variance 2026-04-27 03:08:29 +01:00
Peter Steinberger
aa071e0b60 fix(ollama): forward native model params 2026-04-27 03:08:11 +01:00
Peter Steinberger
f4cf7e3b4f test(docker): recreate update fixture ui asset after install 2026-04-27 03:06:07 +01:00
Peter Steinberger
2dba9e6a76 fix(ollama): honor configured num_ctx params 2026-04-27 03:02:24 +01:00
Peter Steinberger
fc3abc139b fix(cron): classify denied isolated runs 2026-04-27 03:01:55 +01:00
Peter Steinberger
22c9e82e83 test(docker): track update fixture control ui asset 2026-04-27 02:58:24 +01:00
Vincent Koc
8c2bc951a9 fix(plugins): hydrate bundled channel config metadata
Hydrate bundled channel schema metadata through opt-in registry schema paths while keeping ordinary manifest registry loads lightweight.
2026-04-26 18:58:04 -07:00
Peter Steinberger
c45a7d7a7a ci: use available macOS release runner 2026-04-27 02:56:19 +01:00
Vincent Koc
b96a75c95b fix(gateway): scope memory runtime plugin loading 2026-04-26 18:54:59 -07:00
Peter Steinberger
20b71e18b2 test(docker): seed update fixture control ui asset 2026-04-27 02:50:48 +01:00
Peter Steinberger
9b79eef750 fix(memory-core): honor configured index concurrency 2026-04-27 02:47:39 +01:00
Vincent Koc
988cb1ebfe fix(test): stabilize restart sentinel mocks 2026-04-26 18:45:13 -07:00
Vincent Koc
3e020a1650 fix(memory-lancedb): force float embedding encoding (#72391) 2026-04-27 02:43:31 +01:00
Peter Steinberger
5176dba8a0 test(docker): stub update fixture lint preflight 2026-04-27 02:43:15 +01:00
Peter Steinberger
d8c1140235 ci: fix full release validation gh repo context 2026-04-27 02:36:20 +01:00
Peter Steinberger
69daef8246 fix: honor Ollama Modelfile num_ctx discovery 2026-04-27 02:32:30 +01:00
Shadow
3f59cd0a09 Adjust message for stale workflow 2026-04-26 20:31:00 -05:00
pashpashpash
90de4bd855 fix: address successor transcript review follow-ups
Fixes the post-merge review follow-ups from #72471 by deduping stale pre-compaction state entries and preserving parent-before-child ordering for successor transcripts.
2026-04-26 18:27:38 -07:00
Vincent Koc
6a5ecb955c refactor(plugins): drop provider discovery alias 2026-04-26 18:19:05 -07:00
Vincent Koc
eed7b13b62 fix(doctor): scope bundled runtime deps to active plugins 2026-04-26 18:17:56 -07:00
Peter Steinberger
efec8a4a84 docs: note Vitest cache race footgun 2026-04-27 02:17:02 +01:00
Peter Steinberger
bf08dc2ed6 test(docker): fix packaged docker harness lanes 2026-04-27 02:13:56 +01:00
Peter Steinberger
110fa97f2a fix: repair release validation follow-up checks 2026-04-27 02:09:40 +01:00
Peter Steinberger
8c18df02f3 docs: update Ollama fix changelog 2026-04-27 02:08:01 +01:00
Peter Steinberger
e28ad0f84f fix: list configured provider models 2026-04-27 02:08:01 +01:00
Peter Steinberger
c6617c3155 fix: silence Ollama memory doctor key warning 2026-04-27 02:08:00 +01:00
Peter Steinberger
1316ca9aa8 fix: gate Ollama ambient discovery 2026-04-27 02:08:00 +01:00
Peter Steinberger
acfa9877b3 fix: parse Ollama tool call arguments 2026-04-27 02:07:59 +01:00
Peter Steinberger
6a20c83cf7 docs: clarify Ollama web search auth 2026-04-27 02:07:59 +01:00
Peter Steinberger
f0b758fba2 test(docker): stub package-derived update fixture builds 2026-04-27 02:07:29 +01:00
pashpashpash
b99540964c Fix compaction rotation follow-ups 2026-04-26 18:06:57 -07:00
Vincent Koc
b9c7a4306b fix(ci): declare Lobster Ajv runtime dependency 2026-04-26 18:04:46 -07:00
Peter Steinberger
658240de74 ci: add full release validation workflow 2026-04-27 02:02:34 +01:00
Vincent Koc
67d00826b2 fix(gateway): bound Lobster Ajv schema compilation 2026-04-26 17:57:59 -07:00
Peter Steinberger
3c95327b34 Fix compacted session transcript rotation 2026-04-26 17:51:00 -07:00
Vincent Koc
0a117b5960 test(plugins): guard persisted status replay 2026-04-26 17:47:41 -07:00
Peter Steinberger
ddac6f73e5 fix(approvals): accept allowlist metadata 2026-04-27 01:46:30 +01:00
Peter Steinberger
ffbb4d4ae7 test(docker): fix update preflight fixture patches 2026-04-27 01:43:55 +01:00
Peter Steinberger
3937d16c44 fix(exec): fallback when node lacks run prepare 2026-04-27 01:43:03 +01:00
Peter Steinberger
b109c1f99c ci: limit node 22 compatibility to manual ci 2026-04-27 01:39:32 +01:00
Peter Steinberger
92c1924d27 ci: remove duplicate extension fast lane 2026-04-27 01:36:45 +01:00
Peter Steinberger
acd1bd7d31 fix(exec): skip node approval prepare in yolo mode 2026-04-27 01:27:58 +01:00
Peter Steinberger
11e17793e1 ci: include node22 compat in manual full ci 2026-04-27 01:27:27 +01:00
Peter Steinberger
90b3cdb6a7 test(docker): fix update fixture pnpm patch config 2026-04-27 01:25:00 +01:00
Peter Steinberger
7ca2f9fed5 test(docker): align package harness image 2026-04-27 01:22:58 +01:00
Vincent Koc
732a5842ee fix(gateway): defer implicit qmd memory startup 2026-04-26 17:21:50 -07:00
Vincent Koc
d7c173b694 fix(gateway): harden macOS launchd service startup 2026-04-26 17:18:49 -07:00
Peter Steinberger
6fed787297 test: align release boundary expectations 2026-04-27 01:16:15 +01:00
Vincent Koc
7cecbe1002 test(plugins): guard cold status snapshots
Add a reusable cold plugin fixture and status snapshot guard proving read-only plugin metadata paths do not import plugin runtime entries.
2026-04-26 17:15:39 -07:00
Peter Steinberger
0f672dcc73 fix(ollama): align web search endpoint routing 2026-04-27 01:10:41 +01:00
Peter Steinberger
b825c8d34b test: fix full ci suite follow-ups 2026-04-27 01:10:32 +01:00
Peter Steinberger
3b514ad5f3 test(docker): run mounted harnesses with image tsx 2026-04-27 01:05:20 +01:00
Peter Steinberger
82b928232e test(docker): stabilize package update lanes 2026-04-27 01:02:36 +01:00
Peter Steinberger
30d9e70988 test(gateway): stabilize session cleanup gates 2026-04-27 01:02:13 +01:00
Peter Steinberger
a3e0674261 fix(ollama): harden native provider routing 2026-04-27 01:02:13 +01:00
Peter Steinberger
be56f172ab fix: scope qmd root memory collection 2026-04-27 01:01:58 +01:00
Peter Steinberger
d2786fb969 test(docker): run observability harness with global tsx 2026-04-27 00:57:55 +01:00
Peter Steinberger
fa0729e145 test: auto-discover vitest suites 2026-04-27 00:55:06 +01:00
Peter Steinberger
21c51bc140 test(docker): resolve otel decoder from plugin runtime 2026-04-27 00:51:47 +01:00
Vincent Koc
265bc6b6ea test(plugins): guard command cold registry paths
Add command-level sentinel coverage proving channel setup metadata, onboarding auth choices, and models-list provider ownership stay on manifest/registry paths without importing plugin runtime.\n\nLocal verification:\n- pnpm exec oxfmt --check --threads=1 src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm test:serial src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed\n- clean rebase sanity: git diff --check origin/main...HEAD\n\nPR CI had known unrelated main-red failures matching latest main run 24970053892; the new sentinel test passed in CI.
2026-04-26 16:51:36 -07:00
Peter Steinberger
42db865673 test(docker): run observability on shared image 2026-04-27 00:49:36 +01:00
Vincent Koc
5d7c6e6bda test(docker): add observability smoke
Add Docker aggregate observability coverage for QA-lab OTEL and Prometheus diagnostics.
2026-04-26 16:43:56 -07:00
Tak Hoffman
560ddd2f9b Fail package update on unhealthy restart (#72422) 2026-04-26 18:38:23 -05:00
Peter Steinberger
998e37fcb3 ci: allow installer smoke baseline override 2026-04-27 00:31:30 +01:00
Vincent Koc
3cc52d9050 docs(changelog): note codex usage accounting fix 2026-04-26 16:27:23 -07:00
Vincent Koc
7902c769da fix(codex): normalize cached harness input tokens 2026-04-26 16:27:23 -07:00
Peter Steinberger
9be8d43c31 docs: document installer recovery cleanup 2026-04-27 00:26:02 +01:00
Peter Steinberger
eccb79db99 build: remove private QA package compat shims 2026-04-27 00:26:02 +01:00
Peter Steinberger
09a635a28b test: fix main release validation forward-port 2026-04-27 00:07:31 +01:00
Peter Steinberger
5b257cb352 test(qa): drop brittle telegram workflow assertions
(cherry picked from commit b02fdb8264)
2026-04-27 00:07:31 +01:00
Peter Steinberger
efe940e9cb ci(qa): remove telegram beta approval gate
(cherry picked from commit 5e04b0f97a)
2026-04-27 00:07:31 +01:00
Peter Steinberger
8d909ed0da ci(docker): pass beta env to installer e2e
(cherry picked from commit 7677b4ca24)
2026-04-27 00:07:31 +01:00
Peter Steinberger
1bb46ce68a ci(docker): test release installer against beta
(cherry picked from commit d8c4dcb6a4)
2026-04-27 00:07:31 +01:00
Peter Steinberger
54e77a9ec4 ci(docker): use resolved pnpm for scheduled lanes
(cherry picked from commit 61a539a1b7)
2026-04-27 00:07:31 +01:00
Peter Steinberger
43e651db9a ci(docker): preserve pnpm path in scheduler lanes
(cherry picked from commit 2e8a089836)
2026-04-27 00:07:31 +01:00
Peter Steinberger
e7d069edcf test(qa): relax telegram mention reply assertion
(cherry picked from commit 7109251318)
2026-04-27 00:07:31 +01:00
Peter Steinberger
17094640f8 ci(release): trust release branch docker checks
(cherry picked from commit abf0ef9cd3)
2026-04-27 00:07:31 +01:00
Peter Steinberger
16c6a92c53 ci(release): allow npm telegram e2e from release branch
(cherry picked from commit 53f8e9de13)
2026-04-27 00:07:31 +01:00
Peter Steinberger
ef3309a986 fix(release): harden beta validation lanes
(cherry picked from commit 218bceaa14)
2026-04-27 00:07:31 +01:00
Peter Steinberger
95ae3c00bd docs: explain test routing model 2026-04-27 00:05:27 +01:00
Vincent Koc
97e64196a0 fix(hooks): use local timezone for session-memory filenames (#72408) 2026-04-26 16:04:10 -07:00
Peter Steinberger
41ad03dda4 fix(test): allow legacy qa inventory entry 2026-04-27 00:02:33 +01:00
Peter Steinberger
4a578740a2 refactor: deduplicate changed lane detection 2026-04-27 00:02:00 +01:00
Peter Steinberger
20d6daaeaa docs: document automatic bonjour container policy 2026-04-27 00:00:22 +01:00
Peter Steinberger
6018f29dbf ci: keep docker bonjour setting automatic 2026-04-27 00:00:22 +01:00
Peter Steinberger
989cfd1e33 fix(bonjour): auto-disable advertising in containers 2026-04-27 00:00:22 +01:00
Peter Steinberger
89ab39ca64 test: simplify changed test routing 2026-04-26 23:58:13 +01:00
Peter Steinberger
199d5f765f docs(test): explain cheap docker reruns 2026-04-26 23:56:14 +01:00
Peter Steinberger
2fe11020d2 refactor(test): split bundled channel docker scenarios 2026-04-26 23:56:14 +01:00
Peter Steinberger
1ddf6b4e39 ci: skip existing docker e2e images 2026-04-26 23:56:14 +01:00
Peter Steinberger
1a02d00eb4 test: add docker e2e rerun helpers 2026-04-26 23:56:14 +01:00
Peter Steinberger
cfe58387a7 docs: update changelog attribution guidance 2026-04-26 23:51:51 +01:00
Peter Steinberger
6077941d0b fix: restart package updates through updated install 2026-04-26 23:51:51 +01:00
Peter Steinberger
b5714b90ed refactor(test): share docker e2e shell helpers 2026-04-26 23:48:32 +01:00
Peter Steinberger
7a86448a6e ci: reuse docker e2e plan action 2026-04-26 23:48:32 +01:00
Peter Steinberger
6cba12caae test: add docker e2e planner guards 2026-04-26 23:48:32 +01:00
Rubén Cuevas
a08b65a90a fix(telegram): send fresh finals for stale previews (#72038)
* fix(telegram): send fresh finals for stale previews

* test(telegram): cover stale preview send fallback

* fix(telegram): keep stale archived preview fallback

* fix(telegram): clear stale active previews

* fix(telegram): reset preview state after fresh finals
2026-04-26 15:44:30 -07:00
Peter Steinberger
084dde89fd docs: clarify extension ownership boundaries 2026-04-26 23:39:18 +01:00
Peter Steinberger
2efc4a8233 docs(test): document docker e2e layout 2026-04-26 23:36:31 +01:00
Peter Steinberger
cd417f3b68 ci: derive docker e2e artifacts from plan 2026-04-26 23:36:31 +01:00
Peter Steinberger
a2adb05f74 refactor(test): split docker e2e planner 2026-04-26 23:36:31 +01:00
Peter Steinberger
c9c0ab3a44 fix(bonjour): keep ciao failure handling extension-owned 2026-04-26 23:29:40 +01:00
Peter Steinberger
0472b6197a chore: clarify bonjour fatal guard naming 2026-04-26 23:27:35 +01:00
Peter Steinberger
8a60e57846 fix: keep bonjour failures non-fatal 2026-04-26 23:27:08 +01:00
Vincent Koc
c6cf37068c fix(feishu): repair interactive card content extraction (#72397) 2026-04-26 15:26:53 -07:00
Peter Steinberger
ff6044f441 docs(changelog): note Ollama thinking validation fix 2026-04-26 23:25:05 +01:00
Peter Steinberger
5aa3779d8c ci: disable bonjour in install e2e docker 2026-04-26 23:20:08 +01:00
Peter Steinberger
ff9fefb79b fix(agents): validate thinking with model catalog 2026-04-26 23:16:05 +01:00
Peter Steinberger
3746e5b969 ci: cap Telegram E2E build cache 2026-04-26 23:11:21 +01:00
Peter Steinberger
9f5bc5465c style: format codex and loader tests 2026-04-26 23:10:33 +01:00
Peter Steinberger
d108110a89 ci: use packaged tarball for docker e2e 2026-04-26 23:10:33 +01:00
Peter Steinberger
1b1eea238c ci: preserve docker test runner path 2026-04-26 23:04:21 +01:00
Vincent Koc
d9e9e61e77 fix(logging): skip unserializable file log message parts 2026-04-26 15:01:19 -07:00
Vincent Koc
fc0e6e4650 docs(logging): document structured file fields 2026-04-26 15:01:19 -07:00
Vincent Koc
e8df081a1f feat(logging): add file log correlation fields 2026-04-26 15:01:19 -07:00
github-actions[bot]
5c4c33c7de chore(ui): refresh th control ui locale 2026-04-26 22:01:03 +00:00
Vincent Koc
070b55f336 UI: localize command palette labels (#72378) 2026-04-26 14:58:16 -07:00
Vincent Koc
364d49889e fix: allow trusted exec approvals home symlinks (#72377) 2026-04-26 14:57:01 -07:00
Peter Steinberger
baaad52389 ci: split docker e2e images 2026-04-26 22:55:00 +01:00
Peter Steinberger
3a8961af0f test: copy docker build helper in setup e2e 2026-04-26 22:54:27 +01:00
Peter Steinberger
ff570f3a61 fix(ollama): expose native thinking efforts 2026-04-26 22:49:13 +01:00
Peter Steinberger
2cd23957c0 build: use slim docker runtime 2026-04-26 22:47:48 +01:00
Vincent Koc
43a003b8a0 fix: short-circuit live model switch fallback redirects (#72375) 2026-04-26 14:45:02 -07:00
Vincent Koc
fa85e6c26e docs(changelog): note acp stdout fix 2026-04-26 14:42:37 -07:00
Vincent Koc
d46de6cff7 fix(acp): keep server logs off stdout 2026-04-26 14:42:22 -07:00
Peter Steinberger
018f2e78ba build: skip docker apt upgrades 2026-04-26 22:40:44 +01:00
Peter Steinberger
b61954919c ci: verify docker release attestations 2026-04-26 22:40:44 +01:00
Peter Steinberger
5abb717112 docs: add OpenClaw testing skill 2026-04-26 22:40:32 +01:00
Vincent Koc
8226238765 refactor(plugins): share lookup cache eviction 2026-04-26 14:28:15 -07:00
Peter Steinberger
b68b4b9151 ci: add targeted docker lane reruns 2026-04-26 22:27:45 +01:00
Josh Lehman
a3c51f91c5 fix: isolate cron context-engine session keys (#72292) 2026-04-26 14:21:01 -07:00
Vincent Koc
2edbdc42ae refactor(plugins): isolate loader cache state 2026-04-26 14:16:35 -07:00
Peter Steinberger
b28de9a7d9 ci: centralize docker build wrapper 2026-04-26 22:14:36 +01:00
Peter Steinberger
824c3e2b71 ci: enable docker image attestations 2026-04-26 22:14:36 +01:00
Vincent Koc
2194a8c64c docs(logging): document request trace scopes 2026-04-26 14:13:15 -07:00
Vincent Koc
410783c126 fix(diagnostics): chain run traces to request scope 2026-04-26 14:13:15 -07:00
Vincent Koc
3ae6f01d61 feat(logging): propagate request trace scopes 2026-04-26 14:13:14 -07:00
Peter Steinberger
e3cbad4fb6 ci: fix ACPX Docker update repair target 2026-04-26 22:13:00 +01:00
Peter Steinberger
c082cf892a docs: codify formatter tooling 2026-04-26 22:02:31 +01:00
Peter Steinberger
b4a9ac3516 ci: run release Docker chunks through scheduler 2026-04-26 22:02:31 +01:00
Vincent Koc
f0566e410a docs(diagnostics): document model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
c6e9849351 feat(diagnostics): capture model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
8e1755928c refactor(plugins): split plugin registry facade 2026-04-26 13:43:22 -07:00
Vincent Koc
9eb071c3f1 perf(plugins): reuse persisted registry fallback read 2026-04-26 13:43:22 -07:00
Vincent Koc
522eedc754 refactor(plugins): make provider discovery runtime explicit 2026-04-26 13:43:21 -07:00
Vincent Koc
71e361af8a refactor(plugins): split installed plugin index modules 2026-04-26 13:43:21 -07:00
Peter Steinberger
487f8c5d3a test(gateway): skip codex acp bind when auth is unavailable 2026-04-26 21:42:49 +01:00
Peter Steinberger
7a4574376a fix(ollama): honor native model capabilities 2026-04-26 21:40:22 +01:00
Josh Lehman
8ba82534e6 fix: preserve cron telegram topic delivery after timeout (#72317) 2026-04-26 13:30:54 -07:00
Peter Steinberger
ffa84cdc02 ci: chunk release Docker e2e jobs 2026-04-26 21:23:08 +01:00
pash-openai
67ffa3df8b Add Codex Computer Use setup for Codex mode (#71842)
* Add Codex Computer Use setup

* Tighten Codex Computer Use setup checks

* Handle fresh Codex Computer Use marketplace setup

* Fix channel setup manifest fixture

* Match Codex Computer Use marketplace loading

* Harden plugin manifest test fixtures

* Isolate auth choice legacy manifest test

* Update aggregate shard test expectation

* Improve Codex Computer Use first-run setup

* Harden Codex Computer Use auto-install

* Fix plugin auto-enable test fixture roots
2026-04-26 13:21:56 -07:00
Vincent Koc
df542f75a9 fix(logging): expose trace fields in file logs 2026-04-26 12:52:04 -07:00
Peter Steinberger
edf40ab6c9 test(gateway): retry gemini acp startup warmup timeout 2026-04-26 20:50:06 +01:00
Vincent Koc
406ae72fd2 fix(logging): redact persisted transcript text 2026-04-26 12:12:44 -07:00
Peter Steinberger
f99fb2af86 test(gateway): wait longer for codex harness subagent start 2026-04-26 20:11:16 +01:00
Peter Steinberger
244628f467 docs: clarify PR triage comments 2026-04-26 19:48:22 +01:00
Sally O'Malley
637bd33e69 fix(diagnostics): defer OTEL run span finalization (#72260) 2026-04-26 11:29:05 -07:00
Vincent Koc
e53c068d78 fix: repair skills and memory watcher refresh paths 2026-04-26 11:21:21 -07:00
Peter Steinberger
4e181d30fa test(gateway): classify stream fallback as empty live response 2026-04-26 19:15:00 +01:00
Peter Steinberger
e60cc50dff test(gateway): harden acp bind docker smoke 2026-04-26 19:14:58 +01:00
Peter Steinberger
f2dab9b334 fix(agents): keep responses web search reasoning compatible 2026-04-26 19:14:55 +01:00
Peter Steinberger
fc6cfbd418 fix(agents): honor bundle mcp tool allowlist 2026-04-26 19:14:51 +01:00
Vincent Koc
480a3f66c9 fix: shortcut live session model redirects during fallback 2026-04-26 11:14:05 -07:00
Vincent Koc
19e41a1e69 docs(logging): clarify redaction surfaces 2026-04-26 11:09:56 -07:00
Vincent Koc
b4cdd55f62 fix(discord): escalate repeated health-monitor restarts 2026-04-26 11:09:03 -07:00
Vincent Koc
6b6dcafcee fix(webchat): support non-image file attachments 2026-04-26 10:58:24 -07:00
Vincent Koc
303cde8f60 fix(auto-reply): poison inbound dedupe after partial turn failure
* fix(auto-reply): poison inbound dedupe after replay-unsafe failures

* fix(clownfish): address review for ghcrawl-165980-agentic-merge (1)
2026-04-26 10:58:19 -07:00
Vincent Koc
e672b61417 fix(whatsapp): stop reconnecting quiet sockets
Fixes #70678.\n\nKeeps quiet but healthy WhatsApp linked-device sessions connected by tracking WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Also cleans up transport activity listeners on failed connection-open paths.\n\nCarries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.\n\nValidation:\n- pnpm test:serial extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts extensions/whatsapp/src/connection-controller.test.ts\n- pnpm check:changed\n- codex review --base origin/main
2026-04-26 09:51:41 -07:00
Peter Steinberger
4a3030df9e fix: avoid PowerShell error variable collision 2026-04-26 16:26:31 +01:00
Peter Steinberger
30aa1b5223 fix(release): stabilize beta validation lanes 2026-04-26 16:22:12 +01:00
Peter Steinberger
b438a9cc08 test: align Parallels smoke guards 2026-04-26 16:20:58 +01:00
Peter Steinberger
a87edd732d fix: harden Windows Parallels smoke 2026-04-26 16:13:13 +01:00
Peter Steinberger
79ad635515 fix: pass Linux clock sync as epoch 2026-04-26 16:13:13 +01:00
Peter Steinberger
7e51866d23 fix: sync Parallels Linux clock 2026-04-26 16:13:13 +01:00
Peter Steinberger
73affb491a fix: bound dev update cleanup 2026-04-26 16:13:13 +01:00
Peter Steinberger
ddc2036956 fix: stabilize Parallels plugin smoke paths 2026-04-26 16:13:13 +01:00
Peter Steinberger
631552c554 perf: speed up dispatch-from-config tests 2026-04-26 14:14:12 +01:00
Peter Steinberger
dce35b90fe test(release): wait longer for dashboard smoke 2026-04-26 13:53:59 +01:00
Peter Steinberger
fc666cf42a test(qa): allow slower gateway rpc startup retries 2026-04-26 13:51:40 +01:00
Peter Steinberger
67b9167b80 test(extensions): restore transformed dynamic imports 2026-04-26 13:16:05 +01:00
Peter Steinberger
e97bd70264 perf: speed up slow test imports 2026-04-26 13:10:57 +01:00
Peter Steinberger
9089e6b595 fix(cli): keep channel add plugin install noninteractive
# Conflicts:
#	CHANGELOG.md
2026-04-26 12:59:19 +01:00
Peter Steinberger
7e13f3f514 test(plugin-sdk): tighten channel runtime shim scan 2026-04-26 12:17:49 +01:00
Peter Steinberger
760a1525fb docs(plugin-sdk): refresh api baseline 2026-04-26 12:15:14 +01:00
Peter Steinberger
760dd98ddc fix(ci): repair main type and lint failures 2026-04-26 12:09:35 +01:00
Peter Steinberger
ecf71da888 fix(voice-call): avoid duplicate webhook logs 2026-04-26 12:05:34 +01:00
Vincent Koc
8a63c898c8 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  fix(plugins): satisfy doctor compat lint
  chore(plugins): inventory doctor deprecation compat
  fix(plugins): record crabpot compat deprecations
  docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults
2026-04-26 04:05:11 -07:00
Vincent Koc
efaa66f70d fix(plugins): satisfy doctor compat lint 2026-04-26 04:04:27 -07:00
Vincent Koc
4c40cf8783 chore(plugins): inventory doctor deprecation compat 2026-04-26 04:04:26 -07:00
Vincent Koc
6dfb03ab2e fix(plugins): record crabpot compat deprecations 2026-04-26 04:04:26 -07:00
Vincent Koc
3a54bbb617 fix(plugins): persist synthetic auth refs in index 2026-04-26 04:04:11 -07:00
Vincent Koc
2a5d3ad5b9 docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults 2026-04-26 04:04:09 -07:00
Peter Steinberger
a97ee5c1d3 fix(google-meet): recover local chrome tabs 2026-04-26 12:04:00 +01:00
Vincent Koc
647e557869 docs(agent-workspace): rewrite with AccordionGroup for file map, Steps and Tabs for git backup, Warning callouts for sandbox and secret risks 2026-04-26 04:03:00 -07:00
Peter Steinberger
2a26c96000 docs(release): refine beta validation guidance 2026-04-26 12:02:26 +01:00
Vincent Koc
fa4bd05a3a docs(models): rewrite with CardGroup, Steps for selection order, AccordionGroup for picker behavior and merge precedence, ParamField for list/scan flags 2026-04-26 04:01:42 -07:00
Vincent Koc
209522e2e0 docs(model-failover): rewrite with Steps for runtime flow and rotation, AccordionGroup for cooldown buckets and chain rules, Tabs for which errors advance fallback 2026-04-26 03:59:53 -07:00
Vincent Koc
652e8af81e docs(multi-agent): rewrite with Steps for routing tiers, Tabs for common patterns, AccordionGroup for platform examples and tie-breaking 2026-04-26 03:57:19 -07:00
Vincent Koc
c7a0d9b188 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  test(models): stabilize provider index list mocks
  test(cli): cover lazy plugin inspect mocks
  fix(cli): lazy load plugin maintenance paths
  fix(models): keep cold catalog lookup registry indexed
  fix(models): avoid registry for configured list
  fix(cli): lazy load model commands
  fix(ui): remove ineffective dynamic imports
  test: type setup provider mocks
  fix(update): complete channel switch follow-up work
  test(parallels): harden smoke agent model setup
  fix: preserve provider-scoped model options
  fix: keep post-auth model policy cold
  docs: note faster onboarding auth setup
  test: cover setup provider auth selection
  refactor: keep openai setup auth lightweight
  fix: use setup providers for auth choices
  fix: scope provider auth runtime loading
  fix: keep onboarding setup paths cold
  fix: keep onboarding model prompts scoped
2026-04-26 03:51:08 -07:00
Vincent Koc
3013916232 Update docker.md 2026-04-26 03:50:31 -07:00
Vincent Koc
5411f9d217 test(models): stabilize provider index list mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
be388084c2 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
e76bac5d14 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:49:56 -07:00
Vincent Koc
aec1bfa0bb fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:49:43 -07:00
Vincent Koc
8740ca7dee fix(models): avoid registry for configured list 2026-04-26 03:49:43 -07:00
Vincent Koc
23710167cd fix(cli): lazy load model commands 2026-04-26 03:49:43 -07:00
Vincent Koc
3a9463edac test(models): stabilize provider index list mocks 2026-04-26 03:47:25 -07:00
Vincent Koc
fc483ef5d0 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:47:24 -07:00
Vincent Koc
38ea99ec74 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:47:23 -07:00
Vincent Koc
9c25c697dd fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:45:46 -07:00
Vincent Koc
b7533f5112 fix(models): avoid registry for configured list 2026-04-26 03:45:45 -07:00
Vincent Koc
c3a81166fc fix(cli): lazy load model commands 2026-04-26 03:45:45 -07:00
Peter Steinberger
ab0d0f677b fix(ui): remove ineffective dynamic imports
(cherry picked from commit b4ff947206)
2026-04-26 11:45:29 +01:00
Peter Steinberger
06fe67d719 test: type setup provider mocks
(cherry picked from commit ea9da71f03)
2026-04-26 11:41:14 +01:00
Peter Steinberger
6a00be5f90 fix(update): complete channel switch follow-up work 2026-04-26 11:38:44 +01:00
Peter Steinberger
cd8187d7ce test(parallels): harden smoke agent model setup 2026-04-26 11:38:33 +01:00
Shakker
8344fae387 fix: preserve provider-scoped model options 2026-04-26 11:36:32 +01:00
Shakker
3fe0718932 fix: keep post-auth model policy cold 2026-04-26 11:36:32 +01:00
Shakker
cd3b871122 docs: note faster onboarding auth setup 2026-04-26 11:36:32 +01:00
Shakker
edcb2326a1 test: cover setup provider auth selection 2026-04-26 11:36:32 +01:00
Shakker
b11dbb49f9 refactor: keep openai setup auth lightweight 2026-04-26 11:36:32 +01:00
Shakker
44183de706 fix: use setup providers for auth choices 2026-04-26 11:36:32 +01:00
Shakker
3fffa78164 fix: scope provider auth runtime loading 2026-04-26 11:36:32 +01:00
Shakker
2f81c5f580 fix: keep onboarding setup paths cold 2026-04-26 11:36:32 +01:00
Shakker
26b203e573 fix: keep onboarding model prompts scoped 2026-04-26 11:36:32 +01:00
Peter Steinberger
c74fb78194 test: harden cron MCP Docker smoke 2026-04-26 11:33:26 +01:00
Peter Steinberger
cd79e01be3 fix: load default memory plugin at startup 2026-04-26 11:32:58 +01:00
Peter Steinberger
0e490a3c26 fix(plugins): serialize bundled runtime mirrors 2026-04-26 11:32:07 +01:00
Peter Steinberger
4506bb2e02 fix: stabilize channel MCP Docker smoke 2026-04-26 11:31:25 +01:00
Peter Steinberger
74a4ff1adc fix: prefer mounted bundled plugin sources 2026-04-26 11:28:41 +01:00
Peter Steinberger
8a52c7b3d9 test: cover ClawHub plugin install uninstall 2026-04-26 11:28:18 +01:00
Peter Steinberger
3979fce4f9 test: satisfy compat registry lint 2026-04-26 11:28:07 +01:00
Peter Steinberger
8f4f33be78 test: keep compat registry guard-safe 2026-04-26 11:25:02 +01:00
Peter Steinberger
46d74c8f09 docs: update changelog for native require loader (#71122) (thanks @Effet) 2026-04-26 11:23:42 +01:00
Effet
75c9b216e5 fixup! perf(plugins): native-require fast path respects tryNative=false
Review feedback from @chatgpt-codex-connector (P1): callers that pass
`tryNative: false` rely on jiti's alias rewriting (e.g.
`bundled-capability-runtime` in Vitest+dist mode narrows the SDK
slice through shim aliases). Route everything through the jiti
loader when `tryNative` is false so those rewrites still apply.

Review feedback from @greptile-apps (P2): forward the full argument
tuple through to the jiti fallback with `...rest` so any future
loader option argument is not silently dropped by the wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Effet
b40b85c21a perf(plugins): use native require for compiled JS before jiti
Every CLI invocation reads the config snapshot, which pulls bundled
channel doctor contracts and setup surfaces through
`getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds
several seconds of per-load overhead on slower hosts (NAS profiling
shows ~78% of `openclaw config get` wall time spent inside the jiti
library), and that overhead is pure waste for the already-compiled
`.js` artifacts shipped in dist/.

Wrap the loader returned by `getCachedPluginJitiLoader` so that
compiled JS targets go through `tryNativeRequireJavaScriptModule`
first. Jiti stays on the hot path for:
- TS/TSX/MTS/CTS sources
- paths the native-require helper declines (Windows by default, or
  module-resolution fallbacks)

This centralises the fast path that already existed — inside
`doctor-contract-registry` and `channel-entry-contract` — and extends
it to every caller that goes through the jiti loader cache.

Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord
configured):

| command          | before | after |
|------------------|-------:|------:|
| config get X     |    24s |    6s |
| status           |    45s |   18s |
| devices list     |    55s |   26s |
| nodes status     |    55s |   26s |

Fixes the slow config/status/devices/nodes read paths reported in
openclaw#62842. Remaining time is dominated by non-jiti code paths
(config schema validation, eager provider-plugin module eval) that
are out of scope for this patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Vincent Koc
6d60b035b4 chore(plugins): finish compat registry cleanup 2026-04-26 03:17:25 -07:00
Peter Steinberger
bc49fb1cdf test: fix extension dynamic imports 2026-04-26 11:15:45 +01:00
Peter Steinberger
9694c0611c ci: fix main gate 2026-04-26 11:15:45 +01:00
Peter Steinberger
4b2056fcc1 docs: document plugin package entrypoints 2026-04-26 11:12:09 +01:00
Peter Steinberger
a75c3adc4f refactor: centralize plugin update outcome logging 2026-04-26 11:11:58 +01:00
Peter Steinberger
b7404399ef perf: cache bundled runtime dep manifests 2026-04-26 11:11:58 +01:00
Peter Steinberger
f337c9019c refactor: share plugin package entry resolution 2026-04-26 11:11:58 +01:00
Peter Steinberger
8ba9c9098a fix(agents): avoid provider startup scans 2026-04-26 11:11:37 +01:00
Peter Steinberger
8bc4d4bcd4 fix: prevent duplicate chat attachment send races 2026-04-26 11:10:42 +01:00
Vincent Koc
dc05c93c02 chore(docker): expose diagnostics observability settings 2026-04-26 03:05:10 -07:00
Peter Steinberger
4ed97f7e35 docs: update changelog for plugin fixes 2026-04-26 11:01:10 +01:00
Peter Steinberger
f33a812c07 fix: validate plugin package extension entries 2026-04-26 11:01:10 +01:00
Peter Steinberger
d22d6aed16 fix: respect plugin allowlist for bundled deps 2026-04-26 11:01:10 +01:00
Peter Steinberger
93f2d42259 fix: fail plugin update on update errors 2026-04-26 11:01:10 +01:00
Vincent Koc
861cd026d1 docs(release): add plugin deprecation sweep 2026-04-26 02:59:29 -07:00
Peter Steinberger
9a529ca78b chore: update dependencies 2026-04-26 10:54:58 +01:00
Vincent Koc
9f0cd3514c test(plugins): make compat window guard type-safe 2026-04-26 02:52:45 -07:00
Vincent Koc
bb2425e612 test(plugins): enforce compat removal window 2026-04-26 02:51:48 -07:00
Vincent Koc
5baf90ffef chore(plugins): cap compat removal windows 2026-04-26 02:51:48 -07:00
Vincent Koc
3308347a43 fix(security): keep web search credential checks cold 2026-04-26 02:51:48 -07:00
Vincent Koc
22044af066 fix(config): keep command alias validation cold 2026-04-26 02:51:48 -07:00
Vincent Koc
a9d243327c chore(plugins): complete compat registry inventory 2026-04-26 02:51:47 -07:00
Peter Steinberger
975fd5bc8d docs: add gif asset hygiene guidance 2026-04-26 10:48:06 +01:00
Peter Steinberger
bd95baa4f7 fix(bonjour): suppress ciao process crashes 2026-04-26 10:47:36 +01:00
Peter Steinberger
1be39ac847 fix: increase update step timeout 2026-04-26 10:46:55 +01:00
Peter Steinberger
b67d9bf7f0 fix: propagate update timeout to plugin installs 2026-04-26 10:45:11 +01:00
Vincent Koc
d1f40731e3 chore(ci): tune stale assigned triage 2026-04-26 02:42:09 -07:00
Peter Steinberger
4bc5e183ef fix: avoid CLI startup warmup leaks 2026-04-26 10:41:03 +01:00
Vincent Koc
64af2feda0 docs(context-engine): note that uninstalling the selected context engine plugin resets plugins.slots.contextEngine to the default (c6b7444d16) 2026-04-26 02:39:07 -07:00
Vincent Koc
8314b83f9d docs(agents): scope docs-only validation 2026-04-26 02:35:14 -07:00
Peter Steinberger
2aa375149f test: speed up agent hotspot tests 2026-04-26 10:28:04 +01:00
Peter Steinberger
0b301e9af4 fix: avoid eager channel setup loading 2026-04-26 10:27:35 +01:00
Peter Steinberger
6bc5fe6952 fix: harden plugin install and uninstall transactions 2026-04-26 10:27:23 +01:00
Vincent Koc
893f070560 docs(prometheus): rewrite with Steps quick start, Tabs for enable methods and pull-vs-push, AccordionGroup for label policy and troubleshooting; document the 2048-series cap and trusted-operator scope from the diagnostics-prometheus plugin code 2026-04-26 02:26:08 -07:00
Peter Steinberger
9eb0934492 test: tighten changed test routing 2026-04-26 10:25:04 +01:00
Peter Steinberger
87ac8b0456 refactor(discord): use Carbon request client for proxy fetch 2026-04-26 10:20:49 +01:00
Peter Steinberger
a3483acaab fix: stabilize gpt55 qa lab scenarios 2026-04-26 10:18:42 +01:00
Vincent Koc
0f2e7510cb feat(diagnostics-prometheus): add protected metrics exporter 2026-04-26 02:15:33 -07:00
Peter Steinberger
6cd047e7c2 refactor: clean up update and plugin uninstall helpers 2026-04-26 10:07:39 +01:00
Peter Steinberger
d58ede1b34 docs(changelog): keep discord fix scoped 2026-04-26 10:06:38 +01:00
Peter Steinberger
775c61ef5f fix(discord): ignore stale exec approval clicks 2026-04-26 10:06:38 +01:00
Vincent Koc
57a77ecdf9 docs(multi-agent-sandbox-tools): rewrite with CardGroup, AccordionGroup for examples and troubleshooting, Tabs for restrictions, Steps for filter order 2026-04-26 02:00:56 -07:00
Peter Steinberger
382c554786 docs(release): keep 2026.4.26 changelog marker empty 2026-04-26 09:59:42 +01:00
Peter Steinberger
e6c9123262 docs(release): codify beta train backport scan
(cherry picked from commit b7733c48c0)
2026-04-26 09:59:42 +01:00
Vincent Koc
e400295969 docs(cli-gateway): rewrite with CardGroup, ParamField for run/probe/install flags, AccordionGroup for status semantics and probe interpretation 2026-04-26 01:59:27 -07:00
Vincent Koc
da000ce511 docs(changelog): note subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a911eb748b test(qa): cover subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a1b6567059 fix(agents): fallback subagent completion delivery 2026-04-26 01:58:00 -07:00
Vincent Koc
8741a86f93 docs(broadcast-groups): rewrite with AccordionGroup for use cases and best practices, Tabs for strategy and contexts, Steps for message flow 2026-04-26 01:56:29 -07:00
Vincent Koc
ed537edacf docs(twitch): rewrite with Steps for setup, Tabs for install/auth/access patterns, ParamField for account config, AccordionGroup for troubleshooting 2026-04-26 01:55:13 -07:00
Vincent Koc
91666fe194 docs(cli-plugins): rewrite with CardGroup, AccordionGroup for install/update behavior, ParamField for list flags, Tabs for marketplace sources 2026-04-26 01:53:57 -07:00
Peter Steinberger
c6b7444d16 fix(plugins): reset context engine slot on uninstall 2026-04-26 09:50:34 +01:00
Peter Steinberger
42487d0dac fix(update): retry npm updates without optional deps 2026-04-26 09:50:27 +01:00
Peter Steinberger
832bdbc777 fix(update): repair package config after update 2026-04-26 09:50:19 +01:00
Peter Steinberger
d9c5040fc5 docs(tailscale): clarify Control UI pairing 2026-04-26 09:46:59 +01:00
Peter Steinberger
6f50253a4d fix: clarify install switching 2026-04-26 09:46:41 +01:00
Peter Steinberger
aad7b678b0 fix: pass config to plugin command specs 2026-04-26 09:45:05 +01:00
Peter Steinberger
e29d3516bf fix(gateway): skip Tailscale Control UI pairing 2026-04-26 09:42:25 +01:00
Peter Steinberger
5ab5b75348 fix: update Docker plugin registry smokes 2026-04-26 09:42:14 +01:00
Vincent Koc
2652c9eacf fix(configure): defer web search setup runtime
Keep web-search configure and channel command defaults on cold plugin metadata, harden persisted registry reads, and require active config for manifest command defaults.\n\nThanks @vincentkoc
2026-04-26 01:41:57 -07:00
Peter Steinberger
218636a0ea docs(changelog): split 2026.4.25 and 2026.4.26 notes 2026-04-26 09:40:00 +01:00
Vincent Koc
f164b8b357 docs(webchat): note that reasoning-flagged payloads are excluded from WebChat assistant content, transcript text, and audio blocks (4823288b3b) 2026-04-26 01:39:34 -07:00
Vincent Koc
abd5ec98ab fix(runtime): harden dependency install surfaces (#71997)
* fix(runtime): harden dependency surfaces

* fix(runtime): harden dependency install surfaces

* fix(runtime): address dependency surface review

* fix(runtime): address dependency surface review

* fix(channels): avoid read-only plugin loader cycle

* fix(channels): allow optional read-only loader workspace

* test(commands): refresh current main checks

* test(commands): keep provider metadata mock unique

* test(commands): keep doctor security read-only mock unique
2026-04-26 01:38:21 -07:00
Vincent Koc
eb6b35671a docs(changelog): flatten 27 multi-line bullets into single lines per AGENTS.md rule 2026-04-26 01:35:42 -07:00
Peter Steinberger
3b5463591b chore: bump version to 2026.4.26 2026-04-26 09:28:52 +01:00
Peter Steinberger
4ad8b613c9 test: update npm telegram workflow expectations 2026-04-26 09:24:10 +01:00
Peter Steinberger
1969452c3f fix: hide raw agent failures in group chats 2026-04-26 09:19:27 +01:00
Peter Steinberger
134cc64aff fix: keep host plugin registry out of live Docker state 2026-04-26 09:17:38 +01:00
Peter Steinberger
0c020cdb7a test: update ci expectation drift 2026-04-26 09:16:53 +01:00
Peter Steinberger
2f5e5e9a71 fix: break plugin command spec import cycle 2026-04-26 09:15:47 +01:00
Peter Steinberger
1323683d72 fix: stabilize qa lab capture store cleanup 2026-04-26 09:13:30 +01:00
Ayaan Zaidi
7e376e5aba ci: build npm telegram e2e image after approval 2026-04-26 13:39:18 +05:30
Peter Steinberger
e2ef5e2329 test: keep path alias temp dirs out of repo 2026-04-26 09:09:07 +01:00
Peter Steinberger
c99d72575e fix(release): reject staged runtime deps in packs 2026-04-26 09:08:54 +01:00
Shakker
5c0dc93d1e fix(doctor): keep service repair policy scoped 2026-04-26 09:08:36 +01:00
Shakker
6cf5a5fbcd docs: document external service repair policy 2026-04-26 09:08:36 +01:00
Shakker
0b6ebf3343 fix(doctor): honor external service repair policy 2026-04-26 09:08:35 +01:00
Vincent Koc
d24c6095ce docs(sdk-setup): rewrite with Tabs for package metadata and install paths, ParamField for openclaw fields, AccordionGroup for setup-entry rules and helpers 2026-04-26 01:07:59 -07:00
Vincent Koc
64a7a34c83 docs(trusted-proxy-auth): rewrite with Steps for handshake, Tabs for TLS, AccordionGroup for proxy examples and troubleshooting 2026-04-26 01:04:51 -07:00
Vincent Koc
f2744978a0 docs(slash-commands): rewrite with ParamField for config keys, AccordionGroup for command groups and surface notes 2026-04-26 01:02:55 -07:00
Shakker
5037298d82 test: update channel status label fixtures 2026-04-26 09:01:39 +01:00
Shakker
0a82c819bb fix: keep status channel metadata cold 2026-04-26 09:01:39 +01:00
Peter Steinberger
a434133aac fix: fail update on plugin sync errors 2026-04-26 09:01:18 +01:00
Peter Steinberger
4823288b3b fix(gateway): hide webchat reasoning payloads 2026-04-26 09:00:56 +01:00
Peter Steinberger
164aaa48db style: format gateway imports 2026-04-26 09:00:33 +01:00
Peter Steinberger
878e1a2201 fix(plugins): preload cli backend runtime owners 2026-04-26 08:59:41 +01:00
Vincent Koc
6360e1146f docs(media-understanding): rewrite with Steps for behavior and auto-detect, Tabs for config examples and entries, ParamField for attachments 2026-04-26 00:58:31 -07:00
Peter Steinberger
626313a397 fix: satisfy diagnostic trace lint 2026-04-26 08:57:49 +01:00
Peter Steinberger
606a7dbc75 test: stabilize telegram command pagination retry 2026-04-26 08:57:49 +01:00
Peter Steinberger
7cbe271d08 fix: keep channel command defaults read-only 2026-04-26 08:57:49 +01:00
Vincent Koc
06d409dc27 docs(mattermost): rewrite with Steps for setup and HMAC, Tabs for chatmodes, AccordionGroup for slash commands and troubleshooting 2026-04-26 00:56:05 -07:00
Shakker
295bcde7b8 test: update channel metadata mocks 2026-04-26 08:41:34 +01:00
Peter Steinberger
8d50cd82d3 docs(changelog): finalize 2026.4.25 release notes 2026-04-26 08:41:14 +01:00
Vincent Koc
32d3a820c8 docs(sdk-runtime): rewrite with AccordionGroup for runtime namespaces, Steps for store wiring, ParamField for top-level api fields 2026-04-26 00:40:41 -07:00
Vincent Koc
1dc57d4c31 docs(groups): rewrite with Tabs for sandbox patterns and copy-paste intents, AccordionGroup for per-channel notes, Steps for evaluation order 2026-04-26 00:38:20 -07:00
Vincent Koc
fe69b02951 docs(sandboxing): rewrite with Tabs for modes/backends/workspace, AccordionGroup for SSH/OpenShell details, Steps for image setup 2026-04-26 00:35:52 -07:00
Vincent Koc
3e2e26549a docs(cli-config): rewrite with Tabs for set modes, AccordionGroup for builder flags and dry-run details, Steps for the repair loop 2026-04-26 00:32:59 -07:00
Peter Steinberger
4c7a94aac4 fix: quote Windows UI runner paths 2026-04-26 08:31:00 +01:00
Vincent Koc
434c8a1c91 docs(heartbeat): rewrite with Steps for quick start, ParamField for field notes, AccordionGroup for delivery and tasks behavior 2026-04-26 00:30:47 -07:00
Shakker
04575333d3 chore: ignore local agent skills 2026-04-26 08:26:28 +01:00
Shakker
50558e0d56 docs: note channel runtime laziness fixes 2026-04-26 08:26:28 +01:00
Shakker
8fe449c883 fix: avoid channel runtime in format summaries 2026-04-26 08:26:27 +01:00
Shakker
8b32c31252 fix: keep thread placement metadata cold 2026-04-26 08:26:27 +01:00
Shakker
2e101e8413 fix: keep channel security checks cold 2026-04-26 08:26:27 +01:00
Vincent Koc
a77996dc56 fix(diagnostics): propagate trusted traceparent headers 2026-04-26 00:24:47 -07:00
Vincent Koc
5e8fda4c64 docs(memory-config): rewrite with CardGroup overview links, Steps for auto-detect, AccordionGroup for provider configs and QMD subsections 2026-04-26 00:21:28 -07:00
Peter Steinberger
76cf013df5 test: remove slow reply bypass from docker smoke 2026-04-26 08:19:23 +01:00
Vincent Koc
450dc3a206 docs(control-ui): rewrite with Steps for pairing, AccordionGroup for capabilities and chat behavior, Tabs for tailnet access 2026-04-26 00:18:47 -07:00
Peter Steinberger
7b438965bd test: stabilize docker mcp readiness smokes 2026-04-26 08:17:28 +01:00
Peter Steinberger
c5bbf83904 fix: skip unresolved delivery targets for no-deliver cron 2026-04-26 08:17:28 +01:00
Peter Steinberger
f4f74a2391 docs(changelog): organize unreleased notes 2026-04-26 08:15:34 +01:00
Vincent Koc
0c8f0aacf5 docs(plugin-architecture): rewrite with AccordionGroup for shapes and ownership, Steps for the architecture pipeline, Tabs for layering 2026-04-26 00:15:25 -07:00
Peter Steinberger
1de4aff06d fix: cover Windows pnpm and Lobster install regressions 2026-04-26 08:14:28 +01:00
Peter Steinberger
5b9be2cdb1 fix: migrate agent runtime config 2026-04-26 08:12:44 +01:00
Vincent Koc
9d6e79019f docs(secrets): rewrite with Tabs for SecretRef sources, AccordionGroup for providers and exec examples, Steps for the audit flow 2026-04-26 00:12:05 -07:00
Shakker
b5e4e2f257 Revert "fix(plugins): persist registry contribution metadata"
This reverts commit 1ee5654220.
2026-04-26 08:11:09 +01:00
Shakker
59d1fa65df docs: note plugin uninstall file cleanup 2026-04-26 08:11:09 +01:00
Shakker
6428440086 fix: remove plugins from recorded install roots 2026-04-26 08:11:09 +01:00
Peter Steinberger
d419fb561d feat(tts): resolve channel account config generically 2026-04-26 08:10:36 +01:00
Vincent Koc
6c60cd2b72 docs(mcp): rewrite with Steps for lifecycle, Tabs for client modes, ParamField for serve options, AccordionGroup for tools 2026-04-26 00:08:16 -07:00
Vincent Koc
1ee5654220 fix(plugins): persist registry contribution metadata 2026-04-26 00:03:21 -07:00
Peter Steinberger
54f8e4145e test: speed up provider and security tests 2026-04-26 07:59:32 +01:00
Peter Steinberger
d1e5f4bd3c fix(update): bound Windows scheduled task stop 2026-04-26 07:56:46 +01:00
Shakker
3ad29972d0 docs: note read-only channel command discovery 2026-04-26 07:55:00 +01:00
Shakker
43557b16a6 fix: keep channel command discovery read-only 2026-04-26 07:55:00 +01:00
Shakker
fd97f530e3 docs: note cold session metadata fix 2026-04-26 07:55:00 +01:00
Shakker
bbed91bf71 fix: avoid session metadata channel runtime fallback 2026-04-26 07:55:00 +01:00
Shakker
49b106d357 docs: note cold native command defaults 2026-04-26 07:55:00 +01:00
Shakker
7a7728db13 fix: keep native command auto defaults cold 2026-04-26 07:55:00 +01:00
Shakker
aee4c92344 docs: note provider index disablement fix 2026-04-26 07:55:00 +01:00
Shakker
78fb0ade09 fix: honor plugin disablement in provider index rows 2026-04-26 07:55:00 +01:00
Vincent Koc
f48dc96d43 docs(opentelemetry): document harness lifecycle metric, span, and diagnostic events from 82ddcf24f5 2026-04-25 23:54:30 -07:00
Vincent Koc
ff7f0df871 docs(config-tools): rewrite with AccordionGroup for provider examples and field details, ParamField for loop detectors 2026-04-25 23:51:26 -07:00
Peter Steinberger
4ee537a04a fix(node-runtime): keep node-host recovering after gateway restarts 2026-04-26 07:49:45 +01:00
Vincent Koc
c7ead7d8a9 fix(cli): lazy-load model auth runtime 2026-04-25 23:49:06 -07:00
Vincent Koc
62869c8502 docs(bluebubbles): rewrite with Steps for setup, Tabs for DM/groups and coalescing, AccordionGroup for actions and config 2026-04-25 23:48:13 -07:00
Vincent Koc
bb0ef5ef18 docs(troubleshooting): rewrite with AccordionGroup for symptom signatures, Steps for fix flows, and Warning callouts 2026-04-25 23:44:25 -07:00
Harry Xie
77719899f3 fix(gateway): refresh stale embedded service tokens
Refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation.

Fixes #70752.
Thanks @hyspacex.

Co-authored-by: Harry Xie <harryhsieh963@yahoo.com>
2026-04-26 07:42:14 +01:00
Vincent Koc
8c87a637e9 docs(doctor): rewrite with Tabs for headless flags and AccordionGroup for the 19+ detailed behaviors 2026-04-25 23:40:24 -07:00
Vincent Koc
c4a39a6819 docs(model-providers): rewrite with AccordionGroup, CardGroup, Tabs, and Steps for cleaner provider scan 2026-04-25 23:36:01 -07:00
Vincent Koc
82ddcf24f5 feat(diagnostics): add harness lifecycle telemetry 2026-04-25 23:34:34 -07:00
Peter Steinberger
8bbb143ab8 fix: enforce device token scope containment 2026-04-26 07:28:21 +01:00
Peter Steinberger
26e4eb8e40 fix(update): ignore plugin install stages in dist verify 2026-04-26 07:28:02 +01:00
Peter Steinberger
8368026986 fix(installer): preserve PowerShell host on failure 2026-04-26 07:23:48 +01:00
Peter Steinberger
1fae716a04 fix: recover stale cron task records 2026-04-26 07:23:39 +01:00
Ayaan Zaidi
9d21200049 test(e2e): cover npm onboarding runtime deps 2026-04-26 11:53:17 +05:30
Peter Steinberger
7091dbe2bf docs: prefer ghcrawl for OpenClaw issue triage 2026-04-26 07:19:00 +01:00
Vincent Koc
1f267de142 docs(changelog): note cold provider setup guidance 2026-04-25 23:16:59 -07:00
Vincent Koc
585784643e fix(providers): keep setup guidance cold 2026-04-25 23:16:59 -07:00
Peter Steinberger
b979f2964c fix: warn on low disk before runtime dependency staging 2026-04-26 07:16:26 +01:00
Ayaan Zaidi
e633f43c53 fix: strip antml thinking tags in streaming (#69288) (thanks @xialonglee) 2026-04-26 11:46:06 +05:30
Ayaan Zaidi
4bfa7d17a3 refactor(agents): dedupe thinking tag scanner 2026-04-26 11:46:06 +05:30
xialonglee
d7da3d470e fix(agents): strip antml thinking tags in streaming 2026-04-26 11:46:06 +05:30
Peter Steinberger
40e5d9adc7 fix(plugins): update external plugins in recorded root 2026-04-26 07:14:30 +01:00
Vincent Koc
1b99f8aedb docs(diffs): rewrite with Steps, Tabs, ParamField, and AccordionGroup for clearer mode and security guidance 2026-04-25 23:11:13 -07:00
Vincent Koc
eb769ee4ec docs(context-engine): rewrite with Steps, Tabs, AccordionGroup, and ParamField for engine lifecycle clarity 2026-04-25 23:09:26 -07:00
Peter Steinberger
7c6c0a8d54 fix: avoid persisted-auth channel startup probes 2026-04-26 07:09:21 +01:00
Ayaan Zaidi
1ed8c41f33 fix: clear deselected model fallbacks (#71596) (thanks @rubencu) 2026-04-26 11:39:17 +05:30
Ayaan Zaidi
6cc74595e3 refactor(configure): distill fallback selection merge 2026-04-26 11:39:17 +05:30
Ruben Cuevas
1377baee1a fix(configure): clear deselected model fallbacks 2026-04-26 11:39:17 +05:30
Vincent Koc
ce04866019 docs(tasks): rewrite with Tabs for quick start and AccordionGroup for CLI reference and runtime relationships 2026-04-25 23:07:58 -07:00
Peter Steinberger
57c1c7d886 fix(protocol): refresh generated swift models 2026-04-26 07:07:28 +01:00
Vincent Koc
48d83b7566 docs(cron-jobs): rewrite around Steps, Tabs, ParamField, AccordionGroup, and Warning callouts 2026-04-25 23:05:57 -07:00
Peter Steinberger
5a89330c33 fix(installer): fall back from stale nvm dir 2026-04-26 07:04:40 +01:00
Peter Steinberger
e67093f333 chore(plugin-sdk): refresh api baseline 2026-04-26 07:03:25 +01:00
Peter Steinberger
d613c8e29b refactor(tts): resolve voice delivery from channel capabilities 2026-04-26 07:03:25 +01:00
Vincent Koc
2784710f4d docs(acp-agents): rewrite around Steps, Tabs, ParamField, and AccordionGroup for operator runbook clarity 2026-04-25 23:03:14 -07:00
Peter Steinberger
ee2ab9a644 fix(plugins): install optional plugin dependencies 2026-04-26 07:00:16 +01:00
Peter Steinberger
54f4c45e5d fix: stabilize model run probes 2026-04-26 06:59:22 +01:00
Peter Steinberger
6ff7a30b9f fix(providers): guard optional provider index installs 2026-04-26 06:56:42 +01:00
Vincent Koc
cd89adf0ac fix(logging): rotate file logs instead of suppressing 2026-04-25 22:55:33 -07:00
Vincent Koc
e54f5c4068 docs(clawhub): rewrite around Steps Tabs and AccordionGroup
The clawhub doc was 358 lines mixing two install-command bullet
blocks, a 'beginner-friendly' prose walkthrough, six sequential
flat CLI command sections (Auth, Search, Install, Update, List,
Publish skills, Publish plugins, Delete, Sync), and a workflow
section that repeated the same commands a third time.

Restructure for scan-first reading without losing reference detail:

- Wrap Quick start in a 4-step Steps component (search -> install
  -> use -> publish optional CLI install).
- Convert the duplicate native-OpenClaw skills/plugins blocks into
  a single Tabs component with one tab per surface, keeping the
  validation/safety notes inline.
- Convert the service-features bullet list to a 7-row table.
- Move reporting and moderation rules into a 2-panel
  AccordionGroup.
- Convert the eight CLI command sections into one AccordionGroup
  (Auth / Search / Install update list / Publish skills / Publish
  plugins / Delete undelete / Sync) so the flat command catalog
  collapses.
- Convert the global-options bullet list into ParamField
  definitions.
- Consolidate the duplicate workflows section into a single Tabs
  component (Search / Install / Update all / Publish single /
  Sync many / Publish plugin from GitHub).
- Move versioning, lockfile, sync fallback, storage, and telemetry
  notes into a dedicated AccordionGroup.
- Convert the env vars bullet list into a 5-row table.
- Drop the duplicate 'How it works / What you can do / Quick start
  (non-technical)' prose; the same content lives in the new Quick
  start Steps and 'What ClawHub is' summary.
- Sentence-case the Related list and add a missing 'Plugins' link.
- Add sidebarTitle for explicit nav.

CLI flags, command parameters, registry semantics, lockfile path,
moderation thresholds (1-week account age, 20-report cap, 3-report
auto-hide), telemetry env var, and required plugin package.json
metadata are unchanged. Pure restructure plus Mintlify upgrades.
2026-04-25 22:53:54 -07:00
Vincent Koc
50c427efc8 fix(providers): export provider index install types 2026-04-25 22:52:21 -07:00
Vincent Koc
62a5963d24 feat(providers): add provider index install metadata 2026-04-25 22:52:21 -07:00
Vincent Koc
194818960c fix(providers): keep setup flow on cold metadata 2026-04-25 22:52:21 -07:00
Vincent Koc
fd35ba2cad docs(exec-approvals): rewrite around ParamField, Steps, and tables
The exec-approvals doc was 379 lines mixing inspection commands as
free bullets, bullet-list policy knobs (security/ask/askFallback/
strictInlineEval), a long YOLO-mode walkthrough split between two
shell blocks, and a stray dangling HTML comment from a prior split
to the advanced page.

Restructure for scan-first reading without losing operational detail:

- Convert 'Inspecting the effective policy' command bullets into a
  command/result table.
- Convert each policy knob (security, ask, askFallback,
  strictInlineEval) into a ParamField definition so type/values are
  visually distinct.
- Wrap the persistent gateway-host YOLO setup in a Steps component
  (config -> approvals file).
- Move the YOLO 'pick which layer' note and 'auto vs YOLO'
  distinctions into a Warning callout instead of buried inline
  bullets.
- Convert the YOLO layer summary into a 3-row layer/setting table.
- Move the local-only exec-policy limitations into a Note callout.
- Convert allowlist entry fields (id, lastUsedAt, lastUsedCommand,
  lastResolvedPath) into a 4-row table.
- Surface Auto-allow trust caveats as a Warning callout.
- Drop the dangling HTML comment '<!-- moved to /tools/exec-approvals-advanced -->'.
- Sentence-case 'YOLO mode (no-approval)' replacing the inverted
  quote variant ('No-approval YOLO mode').
- Add sidebarTitle for explicit nav.

Trust model, schema example, host approvals JSON shape, allowlist
glob rules, ask-fallback semantics, system-event names, deny-rerun
guard, and the trailing CardGroup of related entries are unchanged.
Pure restructure plus Mintlify component upgrades.
2026-04-25 22:51:59 -07:00
Peter Steinberger
db0864ad41 fix(installer): warn about duplicate global installs 2026-04-26 06:50:33 +01:00
Shakker
d5eae0d959 fix: keep agent provider status on plugin index 2026-04-26 06:49:48 +01:00
Vincent Koc
bf2c992a86 docs(subagents): rewrite around AccordionGroup, ParamField, and Steps
The sub-agents doc was 412 lines of dense bullet lists describing
spawn behavior, tool params, thread binding flow, allowlist rules,
auto-archive behavior, announce semantics, and the sessions_history
sanitization pipeline.

Restructure for scan-first reading without losing reference detail:

- Move spawn-behavior bullets into an AccordionGroup with four
  panels (Non-blocking + push-based; Manual-spawn delivery
  resilience; Completion handoff metadata; Modes and ACP runtime).
- Convert sessions_spawn tool params into ParamField definitions so
  type/default/required render visually.
- Wrap the thread-binding flow in a Steps component (spawn -> bind
  -> route -> inspect timeouts -> detach).
- Convert manual thread controls into a 5-row table.
- Convert allowlist fields (allowAgents, requireAgentId) into
  ParamField definitions.
- Convert announce-context fields into a 6-row source/field table.
- Surface the cost-budget guidance, sessions_spawn delivery-param
  exclusion, operational guidance, and the PAIRING_REQUIRED caller
  caveat as Note/Warning callouts where they were buried inline.
- Sentence-case 'Tool Policy' to 'Tool policy' (heading-case fix).
- Sentence-case 'Configuration Reference' link.
- Alphabetize the Related list and add 'Background tasks' which was
  referenced inline but missing from Related.
- Add sidebarTitle for explicit nav.

Tool surface, depth tables, slash commands, defaults, allowlist
semantics, sandbox-inheritance guard, per-depth tool policy,
auto-archive timing, announce status sourcing, sessions_history
normalization steps, concurrency lane, recovery rules, and
limitations are unchanged. Pure restructure plus Mintlify upgrades.
2026-04-25 22:49:14 -07:00
Peter Steinberger
e69c2853b2 fix(cli): handle Volta shim respawns 2026-04-26 06:48:50 +01:00
Peter Steinberger
e4e69c5bc6 fix: retry systemd unit activation after reload 2026-04-26 06:47:29 +01:00
Vincent Koc
2b29594611 docs(skills): rewrite around tables, ParamField, and AccordionGroup
The skills doc was 409 lines of nested bullet lists describing
precedence, allowlist rules, gating fields, installer specs, and
config overrides. Heavy reference content but no Mintlify structure.

Restructure for scan-first reading without losing reference detail:

- Convert 'Locations and precedence' from a numbered list + arrow
  string into a 6-row precedence table.
- Convert 'Per-agent vs shared skills' bullet/paragraph mix into a
  scope/path/visibility table.
- Move agent-allowlist rules into an AccordionGroup so the example
  config is the headline and the rules collapse on demand.
- Convert ClawHub install/update/sync bullets into a 3-row command
  table.
- Convert SKILL.md frontmatter optional keys (homepage, user-invocable,
  disable-model-invocation, command-dispatch, command-tool,
  command-arg-mode) into ParamField definitions.
- Convert metadata.openclaw fields (always, emoji, homepage, os,
  requires.bins, requires.anyBins, requires.env, requires.config,
  primaryEnv, install) into ParamField definitions.
- Move installer-selection rules and per-installer details (Go, download)
  into an AccordionGroup so the gating section reads as the canonical
  schema plus collapsible operational notes.
- Convert skills.entries config-override rules (enabled, apiKey, env,
  config, allowBundled) into ParamField definitions.
- Surface security caveats as a Warning callout up top instead of a
  bullet list.
- Move 'Looking for more skills?' into the trailing Related list and
  drop the dangling --- separator.
- Sentence-case headings (Format, Gating, Config overrides, Token
  impact) and the Related entries (Creating skills, Skills config,
  Slash commands).
- Drop the redundant 'Skill Workshop' Title-Case heading variant.
- Add sidebarTitle 'Skills' for explicit nav.

Skill source paths, frontmatter parser rules, gating semantics,
installer selection logic, sandboxing notes, env-injection scope,
snapshot/refresh behaviour, remote-node behaviour, and token-impact
formula are unchanged. Pure restructure plus Mintlify components.
2026-04-25 22:45:35 -07:00
Vincent Koc
d54d2d6b9b docs(voice-call): rewrite around Steps Tabs and provider Tabs
The voice-call plugin doc was 664 lines with a flat install/setup
walkthrough, three flat 'Realtime' / 'Streaming' / 'TTS' provider
config blocks each shown twice, an italicised webhook-security
section in Title Case, and a duplicate-Voice Call body H1.

Restructure for scan-first reading without losing operational detail:

- Wrap Quick start in a Steps component (install -> configure ->
  verify -> smoke), with the 'install from npm' vs 'install from
  local folder' choice as a nested Tabs.
- Surface the public-webhook-URL constraint as a Warning at the top
  of Quick start so readers see it before they hit setup.
- Move provider exposure caveats, streaming connection caps, and
  legacy config migration notes into a single AccordionGroup so
  the Configuration section reads as the canonical config plus
  collapsible operational details.
- Convert the Realtime, Streaming, and TTS provider examples to
  Tabs with one tab per provider (Google/OpenAI for realtime;
  OpenAI/xAI for streaming; Core/ElevenLabs/OpenAI override for TTS),
  removing the previous duplicate-block-per-provider pattern.
- Convert the realtime tool-policy bullet list to a 3-row table.
- Convert the agent tool action list and gateway RPC list into
  small tables (action -> args).
- Surface inboundPolicy caller-ID weakness, microsoft-not-supported
  for telephony, and realtime+streaming exclusivity as Warning
  callouts where they were previously buried inline.
- Sentence-case 'Webhook security' (was Title Case), drop the
  duplicate body H1, and refresh the Related list to alphabetical
  sentence-case.

Provider names, env vars, defaults, models, voice ids, command
flags, and field semantics are unchanged. Pure restructure plus
Mintlify component upgrades.
2026-04-25 22:42:47 -07:00
VACInc
78c7292c95 fix: keep telegram tool progress without preview (#71825) (thanks @VACInc)
* fix(telegram): keep default tool progress without preview

* fix: keep telegram tool progress without preview (#71825) (thanks @VACInc)

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-26 11:11:18 +05:30
hcl
c5c40b22af fix(codex): translate minimal thinking for modern models
Fixes #71946
2026-04-25 22:40:53 -07:00
Peter Steinberger
036b422fc6 fix(installer): load nvm before node detection 2026-04-26 06:39:26 +01:00
Peter Steinberger
cbf9c60f1d fix(installer): handle macos gum node failures 2026-04-26 06:37:33 +01:00
Peter Steinberger
be8a3617d9 fix: verify updated gateway version after package restart 2026-04-26 06:37:26 +01:00
Peter Steinberger
142577d9b2 fix(browser): recover stale remote CDP reads safely 2026-04-26 06:35:53 +01:00
Peter Steinberger
eca9f46824 fix: honor node systemd unit activation 2026-04-26 06:35:01 +01:00
Peter Steinberger
33b6962273 fix(installer): fail fast on missing Homebrew Node 2026-04-26 06:33:08 +01:00
Peter Steinberger
257e767e5b fix(telegram): include native quote excerpts for replies 2026-04-26 06:32:46 +01:00
Vincent Koc
639cd50261 fix(models): preserve provider index catalog fallback (#71985)
* fix(models): preserve provider index catalog fallback

* fix(models): mark provider index rows as previews
2026-04-25 22:31:52 -07:00
Shakker
a57d681db9 fix: keep plugin command status on cold index 2026-04-26 06:28:54 +01:00
Vincent Koc
6e3eeb526f docs(video-generation): rewrite around Steps, ParamField, AZ providers
The video-generation page was 454 lines with a 3-step Quick start
written as flat numbered prose, four separate parameter tables (Required,
Content inputs, Style controls, Advanced), the task lifecycle as a
numbered list, and a Related list mixing alphabetic and recency order.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a Steps component (auth -> default model ->
  ask the agent).
- Convert all four parameter tables into ParamField definitions grouped
  under their existing sub-section headings (Required / Content inputs /
  Style controls / Advanced), so types, defaults, and required flags
  show as visual chips and long descriptions wrap cleanly.
- Convert the task lifecycle from a numbered list to a 4-row table for
  at-a-glance scanning.
- Convert Yes/No checkmarks in both the Supported providers and
  Capability matrix tables to ✓ and em-dash, matching the rest of the
  media docs.
- Convert the bullet list under Actions into a 3-row table.
- Sentence-case Related entries and alphabetize the Related list.
- Add sidebarTitle so the nav reads 'Video generation' explicitly.

Schema fields, defaults, model refs, env vars, capability declarations,
fallback rules, and provider notes are unchanged. AccordionGroup of 14
provider notes was already alphabetized and is preserved verbatim.
2026-04-25 22:27:56 -07:00
Peter Steinberger
503a3aa125 fix: defer bedrock discovery sdk import 2026-04-26 06:27:09 +01:00
Peter Steinberger
9f4b155c47 fix(docker): include patch files in runtime image 2026-04-26 06:26:37 +01:00
Peter Steinberger
0e58654dba fix(agents): silence empty group model turns 2026-04-26 06:25:59 +01:00
Vincent Koc
d531760898 docs(music-generation): rewrite around Steps, Tabs, and provider Accordion
The music-generation page was 291 lines with two side-by-side
'Quick start' subsections (shared provider-backed vs. ComfyUI
workflow), a flat parameter table, two prose paragraphs explaining
async behaviour and task lifecycle, and a 'Provider notes' bullet
list mixed with a separate 'Choosing the right path' section.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a top-level Tabs with two child Steps blocks
  (Shared provider-backed | ComfyUI workflow), so readers pick a path
  first and only see the matching steps.
- Convert the tool parameter list to ParamField definitions with
  type signatures and required flags surfaced visually.
- Convert the four async-behaviour bullets to a labelled bullet list
  and the four-state task lifecycle to a table for at-a-glance
  scanning.
- Change Capability matrix Yes/No values to checkmarks/em-dashes for
  alignment with the rest of the media docs.
- Convert the 'Provider notes' free-form paragraphs into an
  AccordionGroup keyed by provider (ComfyUI / Google Lyria 3 /
  MiniMax), keeping wording faithful.
- Sentence-case Related entries and add sidebarTitle so the nav reads
  'Music generation' explicitly.

Provider rows already alphabetized in the supported providers table
(ComfyUI / Google / MiniMax), kept that order. Wording, model refs,
defaults, env vars, and capability declarations are unchanged.
2026-04-25 22:24:58 -07:00
Peter Steinberger
af8648e00e fix(installer): make apt installs noninteractive 2026-04-26 06:23:41 +01:00
Peter Steinberger
58a31b12f7 fix(agents): keep runtime wakeups out of chat transcript 2026-04-26 06:23:27 +01:00
Vincent Koc
f0ea901a0d docs(image-generation): rewrite around Steps, Tabs, and AZ providers
The image-generation page was 395 lines with a 3-step quick-start
written as plain numbered prose, a sprawling 'OpenAI gpt-image-2'
section that mixed routing/legacy/OpenAI options with five inline
slash-command examples, and provider tables that mixed alphabetic
and recency order.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a Steps component (auth -> default model ->
  ask the agent), pulling the Codex OAuth note inline with the model
  step where it belongs and surfacing the LAN/SSRF caveat as a
  Warning callout.
- Alphabetize the Supported providers table (ComfyUI, fal, Google,
  LiteLLM, MiniMax, OpenAI, OpenRouter, Vydra, xAI) and the Provider
  capabilities table (same order across both). Convert the Yes/No
  capability table to checkmarks plus exact counts for readability.
- Replace the long inline OpenAI / OpenRouter / MiniMax / xAI prose
  with a 'Provider deep dives' AccordionGroup so each backend's
  routing, legacy URL handling, and provider-specific knobs collapse
  by default.
- Move the four provider-selection-order notes into a small
  AccordionGroup ('Per-call overrides are exact', 'Auto-detection is
  auth-aware', 'Timeouts', 'Inspect at runtime').
- Collapse the five flat slash-command examples into a single Tabs
  component (4K landscape / transparent PNG / two-square /
  edit-one-ref / edit-multi-ref) with the matching CLI variant inline
  on the transparent-PNG tab.
- Sentence-case the Related list (Tools overview, Configuration
  reference) and drop the redundant generic introductory wording.
- Add sidebarTitle so the nav reads 'Image generation' explicitly.

Wording, schema fields, defaults, model refs, env vars, and the
detailed OpenAI/OpenRouter/Codex routing rules are unchanged.
2026-04-25 22:23:09 -07:00
Vincent Koc
5d3168c343 fix(logging): read config path in bundled runtime 2026-04-25 22:21:15 -07:00
Vincent Koc
d1502c2ba1 docs(media-overview): rewrite around CardGroup, sync/async split, and AZ providers
The media overview was a 91-line page that opened with a redundant
Title-Case body H1 ('# Media Generation and Understanding'), then
mixed a capability table, a Yes/Yes/Yes provider matrix, dense prose
about async behaviour and STT/Voice Call surfaces, plus duplicate
'Quick links' and 'Related' sections at the end.

Restructure for scan-first reading without losing any content:

- Drop the redundant body H1; lead with a one-paragraph summary.
- Replace the 'Capabilities at a glance' table with a CardGroup of six
  entry cards (Image / Video / Music / TTS / Media understanding / STT)
  each linking directly to its dedicated page. Mode (sync/async) is
  noted on the card so readers see latency expectations up front.
- Convert the provider matrix to checkmarks for readability and align
  the column header names. Provider rows already alphabetized.
- Pull async vs synchronous behaviour into a 5-row table that names
  why each capability is sync or async, then keep the operator-facing
  paragraph that explains task-id handoff.
- Move the long 'Google maps to ... OpenAI maps to ... xAI maps to ...'
  paragraph into a per-vendor AccordionGroup so each mapping is a
  collapsible panel instead of one large prose block.
- Drop duplicate 'Quick links' section in favour of a single Related
  list, sentence-cased to match the rest of the docs.
2026-04-25 22:20:35 -07:00
Peter Steinberger
eb5bb67e04 style(macos): satisfy swiftformat 2026-04-26 06:19:35 +01:00
Peter Steinberger
113794f277 fix(voicewake): harden trigger routing rebase 2026-04-26 06:19:35 +01:00
Longbiao CHEN
96988914ff test(gateway): align sessionId voicewake assertion 2026-04-26 06:19:35 +01:00
Longbiao CHEN
dfaa9ee87e fix(gateway): keep explicit agent main sessions stable 2026-04-26 06:19:35 +01:00
Longbiao CHEN
4cc2ffce09 fix(gateway): respect explicit voicewake session targets 2026-04-26 06:19:35 +01:00
Longbiao CHEN
ef7ad8229a fix(voicewake): drop stale sdk collateral 2026-04-26 06:19:35 +01:00
Longbiao CHEN
cbcc1227d3 fix(voicewake): require token boundaries for filler-prefix matches 2026-04-26 06:19:35 +01:00
Longbiao CHEN
e74c079b22 fix(gateway): remove duplicate ws client import 2026-04-26 06:19:35 +01:00
Longbiao CHEN
afe1abc297 feat(voicewake): refresh trigger routing on main 2026-04-26 06:19:35 +01:00
Peter Steinberger
a7382ec563 test: cover older-binary service guard 2026-04-26 06:18:37 +01:00
Vincent Koc
724e92505a docs(tts): add sidebarTitle 'Text to speech (TTS)' for the nav
Default sidebar label fell back to title 'Text-to-speech', which is fine
on the page header but readers scanning the Tools sidebar look for the
acronym 'TTS'. Add a sidebarTitle so Mintlify renders 'Text to speech
(TTS)' in the sidebar while keeping the canonical page title intact.

Sentence case matches the rest of the Tools sidebar group (e.g.
'Image generation', 'Music generation', 'Video generation').
2026-04-25 22:11:31 -07:00
Peter Steinberger
15ea0e1f83 fix(gateway): reserve health probes before route stages 2026-04-26 06:10:49 +01:00
Rubén Cuevas
f9146cabfc fix(telegram): preserve native quote replies
Preserve exact Telegram selected quote text for native quote replies, share Telegram reply parameter construction between bot delivery and direct outbound sends, and retry with legacy replies when Telegram rejects native quote parameters.\n\nThanks @rubencu.
2026-04-26 06:09:43 +01:00
Peter Steinberger
edc3504c77 fix(gateway): accept fnm default path on Linux 2026-04-26 06:09:02 +01:00
Peter Steinberger
8c35e45c00 fix: guard gateway mutations from older binaries 2026-04-26 06:07:55 +01:00
Vincent Koc
fbd6b3ce3c docs(tts): A-Z order providers and add tools/tts to Tools nav group
- docs/tools/tts.md: alphabetize providers in three places that listed
  them: the supported-providers table (Azure Speech ... Xiaomi MiMo),
  the configuration Tabs (12 provider presets in A-Z), and the field
  reference AccordionGroup. Top-level fields stay first; provider
  tabs/accordions follow strict alphabetical order. Wording, schema,
  and defaults unchanged.
- docs/docs.json: add tools/tts to the main Tools sidebar group
  (slotted between trajectory and video-generation, matching the
  alphabetical neighborhood with image-generation, music-generation,
  video-generation). Previously tts only appeared under
  Nodes > Media capabilities, which was a discoverability gap for
  readers looking for TTS alongside the other generation tools.
2026-04-25 22:05:46 -07:00
Vincent Koc
71b79f49ad docs(tts): rewrite tts.md around personas with Mintlify components
The TTS doc had grown to 1008 lines with 11 separate flat 'X primary'
config blocks, a 100-line dense 'Notes on fields' bullet list, and
the new provider-personas feature (#70748) buried near the bottom.
Restructure for readability and feature visibility:

- Lead with a Steps-based 'Quick start' so first-time readers can
  enable TTS in 4 explicit steps.
- Replace the 13-bullet provider list with a single 'Supported
  providers' table that names auth env vars and per-provider notes
  inline. Add a Warning callout for the Microsoft/edge legacy alias.
- Collapse the 11 'X primary' config blocks into one Tabs component
  ('OpenAI + ElevenLabs', 'Google Gemini', 'Azure Speech',
  'Microsoft (no key)', 'MiniMax', 'Inworld', 'xAI', 'Volcengine',
  'Xiaomi MiMo', 'OpenRouter', 'Gradium', 'Local CLI') so users see
  one preset at a time and the page is scannable.
- Promote 'Personas' to its own top-level section with two examples
  (minimal and the Alfred provider-neutral persona), and add a new
  'How providers use persona prompts' AccordionGroup covering Google
  (promptTemplate audio-profile-v1, personaPrompt), OpenAI
  (instructions auto-mapping), and Other providers, plus a fallback
  policy table.
- Note that agents.list[].tts.persona overrides global persona
  per-agent (covers the recent feat(tts) per-agent voice-override
  work).
- Convert the 100-line 'Notes on fields' wall into a per-provider
  AccordionGroup using ParamField, so the field reference is
  scannable and field types/defaults are visually distinct.
- Sentence-case headings, drop redundant body H1, fold the flow
  diagram inline with Auto-TTS behavior, and refresh the Output
  formats section to a table-first layout.
- Schema fields (label/description/provider/fallbackPolicy/prompt
  with profile/scene/sampleContext/style/accent/pacing/constraints
  and providers map) verified against src/config/types.tts.ts; all
  defaults and env-var fallbacks preserved verbatim.

Net diff: 585 insertions, 684 deletions across the same surface
area.
2026-04-25 22:00:19 -07:00
Peter Steinberger
73e2151107 fix: fail updates on activated plugin load errors 2026-04-26 05:57:31 +01:00
Peter Steinberger
ad5c00b8e0 docs: expand bonjour disable troubleshooting 2026-04-26 05:56:25 +01:00
Peter Steinberger
d1a5ea2024 fix(docker): disable bonjour by default for compose 2026-04-26 05:51:05 +01:00
Vincent Koc
4cba24a4c3 fix(logging): redact console and file sinks 2026-04-25 21:50:00 -07:00
Vincent Koc
1a8f765147 docs(changelog): note scoped plugin metadata reads 2026-04-25 21:49:09 -07:00
Vincent Koc
b7340ec6a9 fix(plugins): scope metadata manifest reads 2026-04-25 21:48:47 -07:00
Shakker
3ea20d1413 fix: harden cold plugin metadata paths 2026-04-26 05:48:10 +01:00
Vincent Koc
9c8245b178 docs(agents): clarify clean rebase gate reuse 2026-04-25 21:47:55 -07:00
Peter Steinberger
27aedcfd56 style: format repository 2026-04-26 05:47:12 +01:00
Peter Steinberger
6a67f65568 fix(voice): reuse preflight transcripts across channels 2026-04-26 05:42:04 +01:00
Vincent Koc
46b9044c3f docs: update model input modalities and OTEL token-metric attrs
Two recent commits added user-facing surface that left signature-style
references in docs stale:

- 4428661779 Alvin Tang (#20721, thanks @alvinttang) extends the
  configured model 'input' modality set to also accept 'audio' and
  'video', matching what providers like LM Studio already report.
  docs/plugins/manifest.md model-fields table listed only
  'text | image | document', so add 'audio' and 'video'.
- 44da034516 Vincent (thanks @oc-factus) adds a bounded openclaw.agent
  attribute on the openclaw.tokens counter so per-agent dashboards can
  group usage. docs/gateway/opentelemetry.md metric reference omitted
  it; add it to the attrs list.
2026-04-25 21:39:44 -07:00
Peter Steinberger
9b93b7df62 fix(whatsapp): remove ack reactions after replies 2026-04-26 05:36:14 +01:00
Peter Steinberger
427e485f76 fix(update): verify restarted gateway version 2026-04-26 05:35:45 +01:00
Peter Steinberger
6893e8f5f4 test(gateway): stabilize agent wait lifecycle test 2026-04-26 05:34:33 +01:00
Peter Steinberger
5f2273e81e fix(gateway): unify chat display projection 2026-04-26 05:33:58 +01:00
Neerav Makwana
dc9ce2a1bf fix: honor agent for models auth writes (#71933)
Honor the parent `models auth --agent <id>` flag across auth write commands: `add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.

The auth helpers now resolve the requested configured agent before choosing the auth-profile store and provider workspace, while preserving default-agent behavior when `--agent` is omitted.

Validation:
- `pnpm test src/cli/models-cli.test.ts src/commands/models/auth.test.ts`
- `pnpm test src/commands/models/auth.test.ts`
- `pnpm docs:check-mdx`
- `pnpm check:changed`
- `pnpm check`
- `pnpm build`
- `pnpm test src/cli/run-main.test.ts`

Full `pnpm test` was also run; it failed in unrelated `src/cli/run-main.test.ts` assertions during the full-suite order, while the exact file passes on both latest main and this branch. The PR diff only touches models auth CLI/auth files, docs, and changelog.

Fixes #71864.

Thanks @neeravmakwana.
2026-04-26 05:30:47 +01:00
Vincent Koc
1252da325f fix(tests): remove duplicate config mock key 2026-04-25 21:27:39 -07:00
Peter Steinberger
ae45eebef1 fix: route remote mac browser through node host 2026-04-26 05:25:59 +01:00
Peter Steinberger
b8aef04ccd docs(config): refresh config baseline hash 2026-04-26 05:20:45 +01:00
Alvin Tang
4428661779 fix(config): accept video and audio model inputs
Preserve configured audio/video model input modalities through provider catalog normalization.\n\nFixes #20721.\nThanks @alvinttang.
2026-04-26 05:18:54 +01:00
Peter Steinberger
f1eef47839 fix(agents): treat empty group replies as silent 2026-04-26 05:17:02 +01:00
Shakker
c953e98c59 docs: clarify provider index foundation scope 2026-04-26 05:14:51 +01:00
Shakker
89f368e2f9 test: exercise unsafe provider index keys 2026-04-26 05:14:51 +01:00
Shakker
e827778129 fix: keep provider index previews authoritative 2026-04-26 05:14:51 +01:00
Shakker
911172e1e6 fix: avoid provider index preview row spread 2026-04-26 05:14:51 +01:00
Shakker
f1e28370c4 docs: explain provider index authority 2026-04-26 05:14:51 +01:00
Shakker
96ac51d23d feat: add model catalog provider index contract 2026-04-26 05:14:51 +01:00
Peter Steinberger
ac0fa474f8 fix(gateway): tolerate void agent command results 2026-04-26 05:14:36 +01:00
Eulices
008e4ca81f fix: add placeholder transcript for silent voice notes (#49131)
* fix: add placeholder transcript for silent voice notes

* fix: handle placeholder transcripts per skipped attachment

* fix: preserve synthetic transcript attachment order

* fix: scope synthetic audio merge to audio slice only, preserve cross-capability and prefer ordering

Replace the global outputs.sort() with a targeted merge that:
1. Only sorts within the audio output slice (real + synthetic),
   preserving CAPABILITY_ORDER and per-capability attachments.prefer
   ordering for non-audio outputs.
2. Excludes synthetic placeholder indexes from audioAttachmentIndexes
   used by extractFileBlocks, so tiny audio-MIME files with text
   extensions can still be recovered via forcedTextMime.

Adds mergeAudioOutputsPreservingAttachmentOrder helper.

* fix: remove unused function and use toSorted() for oxlint compliance

* fix(media-understanding): preserve selected audio order for synthetic placeholders

- merge synthetic skipped-audio placeholders using audio decision order
  instead of raw attachmentIndex sorting, preserving attachments.prefer
- insert synthetic-only audio outputs at the audio capability slot
  (before video) when no real audio outputs were produced

* fix(media-understanding): use neutral too-small placeholder text

Clarify that this synthetic transcript path is triggered by attachment size,
not by a silence/no-speech detection result.

* test(media-understanding): update too-small audio placeholder expectations

* test(media-understanding): cover mixed too-small audio placeholder

* test(media-understanding): cover too-small audio context

* fix(tasks): preserve visible task title before internal context

* Revert "fix(tasks): preserve visible task title before internal context"

This reverts commit dc536fb4d3c8a01168de5d05e8562193dd68a88e.

---------

Co-authored-by: Eulices Lopez <eulices@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-26 05:14:01 +01:00
Ayaan Zaidi
bcc9fc4cf5 docs: move TTS persona changelog entry 2026-04-26 09:42:38 +05:30
Ayaan Zaidi
cc2044633c fix(tts): compose personas with agent config 2026-04-26 09:42:38 +05:30
Barron Roth
f801fe7d27 test: update provider HTTP mocks 2026-04-26 09:42:38 +05:30
Barron Roth
9975de89d1 test: fix PR CI shards 2026-04-26 09:42:38 +05:30
Barron Roth
f7c837b374 TTS: remove persona rewrite placeholder 2026-04-26 09:42:38 +05:30
Barron Roth
0594fa3c4d TTS: add provider personas 2026-04-26 09:42:38 +05:30
Peter Steinberger
80219ed1b3 fix(channels): tolerate sparse channel metadata during validation 2026-04-26 05:06:52 +01:00
likewen-tech
86328585fa fix(tasks): terminalize gateway agent run ledger
Terminalize Gateway-backed async task records from the run result while preserving aborted, failed, cancelled, and lost outcomes.\n\nThanks @likewen-tech.
2026-04-26 05:06:33 +01:00
Ayaan Zaidi
f9c8a5107c fix(cli): cap fresh session reseed size 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
8559a84e4e fix(cli): bound fresh session reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
12e4841d96 fix(cli): preserve prompt hooks in history reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
0ba28c0911 docs(changelog): note CLI compaction fix 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
3eff589ac0 test(cli): cover transcript compaction reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
dfd5940c34 fix(cli): compact persisted CLI transcripts 2026-04-26 09:34:24 +05:30
Peter Steinberger
b277eac656 fix: pin macos ssh remote url to loopback 2026-04-26 05:01:25 +01:00
Peter Steinberger
9ed11d6c49 fix: steer agents to safe gateway config flow 2026-04-26 05:00:17 +01:00
Vincent Koc
44da034516 fix(otel): add agent label to token metrics 2026-04-25 20:57:47 -07:00
Shakker
d251932fcf refactor: keep manifest contracts on plugin index 2026-04-26 04:55:12 +01:00
Vincent Koc
948c32dd33 fix(channels): lazy-load setup fallback runtime 2026-04-25 20:53:44 -07:00
Ayaan Zaidi
acd3d2b197 fix: reflect Claude CLI auth status (#71332) (thanks @neeravmakwana) 2026-04-26 09:21:06 +05:30
Ayaan Zaidi
76dc66f5fa fix(models): use synthetic auth expiry for status 2026-04-26 09:21:06 +05:30
Neerav Makwana
ad27e0069d fix(models): avoid externalizing Claude CLI auth 2026-04-26 09:21:06 +05:30
Neerav Makwana
911fcb47f1 fix(models): reflect Claude CLI auth status 2026-04-26 09:21:06 +05:30
Peter Steinberger
c9e7bfd1fc fix(mac): implement swift-log event handler 2026-04-26 04:48:43 +01:00
Peter Steinberger
29741f696a fix(feishu): transcribe inbound voice notes 2026-04-26 04:47:45 +01:00
Peter Steinberger
38e61e0046 test(plugins): drop duplicate bundle command mock 2026-04-26 04:46:05 +01:00
Peter Steinberger
540c70d166 fix(plugins): ignore bundled load path aliases 2026-04-26 04:46:05 +01:00
Shakker
42f87c07e9 docs: add model list catalog changelog 2026-04-26 04:41:51 +01:00
Shakker
26a647d4bb docs: scope manifest model list note 2026-04-26 04:41:51 +01:00
Shakker
0f27f2b351 feat: use indexed manifests for static model catalog rows 2026-04-26 04:41:51 +01:00
Shakker
469bd5f51e docs: mention manifest model list rows 2026-04-26 04:41:51 +01:00
Shakker
4a195b37d5 feat: declare deepseek manifest model catalog 2026-04-26 04:41:51 +01:00
Shakker
8749f1deb4 feat: declare moonshot manifest model catalog 2026-04-26 04:41:51 +01:00
Shakker
35171f4e47 feat: use manifest catalog rows for provider list fast path 2026-04-26 04:41:51 +01:00
Shakker
82a529aaaf feat: carry manifest catalog discovery mode 2026-04-26 04:41:51 +01:00
Peter Steinberger
9e4a0e7f3c fix(qqbot): ignore bot self-echo events 2026-04-26 04:40:53 +01:00
Peter Steinberger
e40094a9ef test(browser): add CDP snapshot Docker smoke 2026-04-26 04:40:26 +01:00
Peter Steinberger
4edf22f63f fix(acpx): avoid startup agent probes by default 2026-04-26 04:40:26 +01:00
Peter Steinberger
ed1ac2fc44 feat(browser): add CDP role snapshot fallback 2026-04-26 04:40:26 +01:00
Peter Steinberger
0ca9c4dcb0 fix(cli): preserve lazy placeholder options 2026-04-26 04:40:26 +01:00
Peter Steinberger
e74f2e1501 test: remove duplicate plugin enable mock 2026-04-26 04:39:54 +01:00
Shakker
2d68fda31f fix: defer onboarding install record commits 2026-04-26 04:39:12 +01:00
Shakker
34bd66d929 fix: keep read-only channels off setup runtime 2026-04-26 04:39:12 +01:00
Shakker
2e7635f4f9 fix: scope web provider ownership to plugin index 2026-04-26 04:39:12 +01:00
Peter Steinberger
6d4f65c9d4 docs: clarify codex runtime routing 2026-04-26 04:38:39 +01:00
Peter Steinberger
6336ed4166 fix: gate codex acp route hints 2026-04-26 04:36:26 +01:00
Peter Steinberger
b58223510c fix(providers): support zai preserved thinking 2026-04-26 04:35:50 +01:00
Peter Steinberger
844d2bd515 test: mock browser cleanup in heartbeat session tests 2026-04-26 04:33:37 +01:00
Colin Johnson
21082d2ede fix(plugins): verify bundled runtime deps installs
Verify bundled runtime dependency installs before reporting success, so a clean npm exit cannot hide packages missing from the managed runtime-deps root.

Also updates the bundle command test mock for the current plugin enable-state API.

Local proof:
- `pnpm test src/plugins/bundle-commands.test.ts`
- `pnpm test src/plugins/bundled-runtime-deps.test.ts src/commands/doctor-bundled-plugin-runtime-deps.test.ts src/plugins/loader.test.ts`
- `pnpm check:changed`

Co-authored-by: Colin <colin@solvely.net>
2026-04-26 04:32:33 +01:00
Peter Steinberger
96d90091c4 test: align task title sanitization expectation 2026-04-26 04:30:13 +01:00
Peter Steinberger
2c8c79de5c fix(tts): normalize streamed tts voice media 2026-04-26 04:28:19 +01:00
Peter Steinberger
f4e6322649 test: align runtime schema registry mock count 2026-04-26 04:25:41 +01:00
Peter Steinberger
924e132d96 test: add manifests to npm install fixtures 2026-04-26 04:22:28 +01:00
Pinghuachiu
7b943667a0 fix: expose image edit geometry flags in capability cli
Expose image edit geometry flags in the capability CLI and document the new infer options.\n\nThanks @Pinghuachiu.
2026-04-26 04:22:22 +01:00
Peter Steinberger
ee8f41f56e fix(channels): strip copied inbound metadata from replies 2026-04-26 04:21:20 +01:00
Vincent Koc
7fef13abbc docs(anthropic): note context1m param applies to Claude CLI backend
Ayaan's 28e4cd81a9 (#70863, thanks @bidadh, source from Arthur Kazemi
8abbae0101) extended params.context1m:true so the configured 1M
context window override now applies to eligible Claude CLI Opus and
Sonnet models, not only direct API calls. CHANGELOG entry covered
the change but docs/providers/anthropic.md '1M context window (beta)'
Accordion only described direct-API behavior, so Claude CLI users had
no signal the same param works for their backend. Add a sentence
inside the same Accordion.
2026-04-25 20:18:51 -07:00
Peter Steinberger
b3ac316e0b fix: preserve indexed plugin diagnostics 2026-04-26 04:17:15 +01:00
Shakker
862b39976d fix: remove managed plugin files on uninstall 2026-04-26 04:16:33 +01:00
Shakker
48ba3a4198 fix: clean migrated plugin install config 2026-04-26 04:16:33 +01:00
Shakker
f5f4477bae fix: reject manifestless plugin archives 2026-04-26 04:16:33 +01:00
Ayaan Zaidi
28e4cd81a9 fix: enable claude cli context1m window (#70863) (thanks @bidadh) 2026-04-26 08:45:59 +05:30
Ayaan Zaidi
64630e1c39 test: fix claude cli context1m fixture 2026-04-26 08:45:59 +05:30
Arthur Kazemi
8abbae0101 fix: enable claude-cli 1m context override 2026-04-26 08:45:59 +05:30
Arthur Kazemi
bb389a37d0 test: cover claude-cli context1m context-window behavior 2026-04-26 08:45:59 +05:30
Peter Steinberger
a91baa16de fix(tts): honor explicit directive providers 2026-04-26 04:14:48 +01:00
Peter Steinberger
969a3757b9 test: harden plugin registry mocks 2026-04-26 04:10:48 +01:00
Peter Steinberger
cf834e2a21 fix(tts): clean streamed directive text 2026-04-26 04:09:56 +01:00
Vincent Koc
2261918c8c fix(plugins): resolve activation plans from plugin registry 2026-04-25 20:06:06 -07:00
Peter Steinberger
6df120fb39 fix: keep internal completion wakes out of chat memory 2026-04-26 04:01:45 +01:00
Shakker
d0d93d0fde test: harden bundle index reconstruction 2026-04-26 03:58:29 +01:00
Peter Steinberger
8748ae3bb7 fix(skills): parse remote which bin maps 2026-04-26 03:58:22 +01:00
Peter Steinberger
18a638ceae test: isolate cold plugin registry metadata mocks 2026-04-26 03:57:38 +01:00
Ayaan Zaidi
a8b4be0b48 fix: unwrap nested Claude CLI results (#66819) (thanks @mraleko) 2026-04-26 08:27:33 +05:30
Ayaan Zaidi
1c77515396 fix(agents): scope Claude JSON unwrapping 2026-04-26 08:27:33 +05:30
Alec Hrdina
1b41513b3b agents/cli: scope nested result unwrapping 2026-04-26 08:27:33 +05:30
Alec Hrdina
015e39e3cf agents/cli: unwrap nested claude result json 2026-04-26 08:27:33 +05:30
Peter Steinberger
c3833f7729 fix(providers): satisfy vllm chat kwargs lint 2026-04-26 03:54:20 +01:00
Peter Steinberger
ed5276f9b9 fix(providers): keep vllm nemotron replies visible 2026-04-26 03:54:20 +01:00
Peter Steinberger
7a85c1a822 fix(tts): surface voice status and harden providers 2026-04-26 03:51:30 +01:00
Peter Steinberger
1231f21679 fix(plugins): tolerate partial manifest registry records
# Conflicts:
#	src/plugins/installed-plugin-index.ts
2026-04-26 03:50:40 +01:00
Peter Steinberger
f5812aa64d test: complete plugin install manifest fixtures 2026-04-26 03:50:40 +01:00
Peter Steinberger
0cf30b6a65 docs(tasks): document retained lost task audit 2026-04-26 03:50:40 +01:00
Likewen
de5b173546 fix(tasks): normalize task timestamps and retained lost audit
Normalize task lifecycle timestamps on create, update, and restore so startedAt/lastEventAt/endedAt cannot precede createdAt in audit-visible records.

Downgrade retained lost tasks with future cleanupAfter from audit errors to warnings while keeping expired or unstamped lost tasks as errors.

Verification: pnpm exec oxfmt --write --threads=1 src/tasks/task-registry.ts src/tasks/task-registry.test.ts src/tasks/task-registry.audit.ts src/tasks/task-registry.audit.test.ts

Verification: node scripts/test-projects.mjs src/tasks/task-registry.test.ts src/tasks/task-registry.audit.test.ts (task-registry.audit.test.ts 4 passed; task-registry.test.ts 45 passed)
2026-04-26 03:50:40 +01:00
Shakker
d955bf0ff8 fix: scope cold plugin manifests to index 2026-04-26 03:47:46 +01:00
Shakker
1a193b2d96 fix: scope cold plugin manifests to index 2026-04-26 03:47:45 +01:00
Peter Steinberger
f8a677bcfd docs: reduce changelog rebase churn 2026-04-26 03:46:31 +01:00
Peter Steinberger
0ddbae171d test: cover codex app-server subagents 2026-04-26 03:46:30 +01:00
Vincent Koc
c149de7750 fix(plugins): resolve runtime metadata fallbacks cold 2026-04-25 19:45:59 -07:00
Peter Steinberger
07877d71cd fix(control-ui): preserve optimistic messages on empty history 2026-04-26 03:45:07 +01:00
Peter Steinberger
97ae1c7c2e feat(tts): add read-latest voice command 2026-04-26 03:44:44 +01:00
Vincent Koc
2235a13dab fix(plugins): resolve capability metadata cold 2026-04-25 19:43:07 -07:00
Peter Steinberger
3989510251 docs: expand ACP agents guide 2026-04-26 03:42:44 +01:00
Peter Steinberger
e23d17da79 test: update full-suite planner expectation 2026-04-26 03:39:52 +01:00
Vincent Koc
d8ed49f651 fix(plugins): keep config alias normalization cold 2026-04-25 19:39:41 -07:00
Peter Steinberger
f0fa35082b fix: keep ACP completion prompts harness-safe 2026-04-26 03:39:24 +01:00
Vincent Koc
4fbc490fca fix(agents): resolve provider attribution metadata cold 2026-04-25 19:36:28 -07:00
Ayaan Zaidi
23fbdc1ec2 fix: allow large Claude live JSONL lines (#71897) 2026-04-26 08:06:06 +05:30
Ayaan Zaidi
09e60e496b fix(agents): allow large Claude live JSONL lines 2026-04-26 08:06:06 +05:30
Ayaan Zaidi
78e0976f93 test(agents): cover large Claude live JSONL output 2026-04-26 08:06:06 +05:30
Peter Steinberger
802a73a382 test: isolate legacy auth choice aliases 2026-04-26 03:34:11 +01:00
Vincent Koc
10763781fd fix(config): resolve plugin contracts cold 2026-04-25 19:33:56 -07:00
Vincent Koc
a0ca546997 test(qa): add local otel smoke harness 2026-04-25 19:30:46 -07:00
Vincent Koc
476bb38527 fix(setup): plan setup metadata from plugin registry 2026-04-25 19:30:20 -07:00
Peter Steinberger
72d8600eb5 test: fix plugin skills mock typing 2026-04-26 03:28:59 +01:00
Peter Steinberger
6855b33255 docs(tts): clarify WhatsApp voice-note delivery 2026-04-26 03:28:51 +01:00
Vincent Koc
bc24b547d0 fix(agents): resolve plugin skill metadata cold 2026-04-25 19:26:21 -07:00
Peter Steinberger
0796a888ae fix: tolerate partial plugin registry records 2026-04-26 03:26:11 +01:00
Peter Steinberger
9b91040053 fix(tts): route WhatsApp MP3 TTS as voice notes 2026-04-26 03:26:00 +01:00
Peter Steinberger
90cd9fce85 fix(agents): handle empty Claude stop turns 2026-04-26 03:23:16 +01:00
Vincent Koc
a44a3f9171 fix(auth): resolve plugin auth metadata cold 2026-04-25 19:22:31 -07:00
Ayaan Zaidi
bbd9702077 ci: fix Docker E2E image build 2026-04-26 07:49:04 +05:30
Vincent Koc
6afac5208a fix(secrets): resolve plugin env metadata cold 2026-04-25 19:18:30 -07:00
Peter Steinberger
c14d2b0c1f fix: harden plugin registry contribution lookup 2026-04-26 03:17:31 +01:00
Peter Steinberger
2d9a0d9cf0 fix: preserve image history while pruning replay context 2026-04-26 03:16:00 +01:00
Peter Steinberger
69e7e499b1 docs(tts): document per-agent tts config 2026-04-26 03:13:43 +01:00
Vincent Koc
690046637f fix(web): resolve provider candidates from plugin registry 2026-04-25 19:13:15 -07:00
Peter Steinberger
9b4f0779ce fix(tts): honor per-agent config in tts commands 2026-04-26 03:12:30 +01:00
Vincent Koc
6a688e33f6 docs(agents): require single-line changelog bullets
Add an explicit rule under Docs / Changelog that bullets must stay on
one line — no wrapping or continuation across multiple lines.
Justification: dedupe, PR-ref, and credit-audit tooling assumes
single-line entries, and the rest of the file is already uniform
single-line. The recent flatten pass collapsed 80 multi-line bullets
in last two releases plus Unreleased; this rule prevents that drift
from coming back through new entries.
2026-04-25 19:10:21 -07:00
Peter Steinberger
0e1f53f020 fix: clear system events on session reset 2026-04-26 03:09:15 +01:00
Vincent Koc
d65f28f962 docs(changelog): flatten 80 multi-line bullets in last two releases and unreleased
Many bullets in Unreleased, 2026.4.25 (Unreleased), 2026.4.24, and
2026.4.23 wrapped across two to five lines, mixing single-line and
multi-line entries within the same section. The repo convention is
single-line bullets — that is what every author/PR reformatter aims
at, what the dedupe and PR-ref scanners assume, and what readers
scrolling the file see in the rest of the changelog.

Reflow each multi-line bullet to a single line by joining
continuation lines on a single space and collapsing redundant
whitespace. Wording, casing, links, PR refs, and Thanks credits are
unchanged — the only edit is layout.

Skipped 2026.4.22 and earlier on purpose; this pass is scoped to
'last two releases plus unreleased' as requested.
2026-04-25 19:07:57 -07:00
Vincent Koc
e4199379ff fix(channels): resolve cold channel presence from registry 2026-04-25 19:07:05 -07:00
Peter Steinberger
94316334fe test: use agent wait for OpenAI web search smoke 2026-04-26 03:06:09 +01:00
Peter Steinberger
a6d9926d1d fix: keep acp management commands local 2026-04-26 03:02:04 +01:00
Peter Steinberger
9123c8158d test: stabilize main ci provider tests 2026-04-26 03:00:57 +01:00
Shakker
0f343ad568 fix: make plugin install config migration atomic 2026-04-26 02:56:44 +01:00
Peter Steinberger
04e08cea62 chore(tts): refresh plugin sdk api baseline 2026-04-26 02:54:13 +01:00
Peter Steinberger
0ca952cdd5 feat(tts): add per-agent voice overrides 2026-04-26 02:54:13 +01:00
Peter Steinberger
1bc9bada65 test: speed up security audit tests 2026-04-26 02:51:19 +01:00
Peter Steinberger
ec56dd3116 fix(pairing): preserve corrupt pairing stores 2026-04-26 02:50:19 +01:00
Vincent Koc
5469740170 fix(github): exempt maintainers from barnacle candidate labels 2026-04-25 18:49:44 -07:00
Shakker
105785a1be fix: preserve channel plugin install records 2026-04-26 02:48:29 +01:00
Peter Steinberger
e3be66ddda test: align plugin docker smoke with install ledger 2026-04-26 02:46:04 +01:00
Peter Steinberger
75a8f5863c test: speed up chat UI tests 2026-04-26 02:45:31 +01:00
Vincent Koc
526fd9d545 test: stabilize pty and agentic test lanes 2026-04-25 18:44:18 -07:00
Vincent Koc
d74f897c1c fix(status): keep channel status reads cold 2026-04-25 18:40:38 -07:00
Vincent Koc
839e7c98ff fix(channels): keep read-only plugin listing cold 2026-04-25 18:40:38 -07:00
Vincent Koc
e40157013f fix(diagnostics): complete early model stream closes 2026-04-25 18:40:00 -07:00
Vincent Koc
c7b336d83e docs(hooks): document new ctx.jobId field on plugin hook contexts
Scott Glover's commit 371b69b3e2 ('Expose cron jobId in plugin hook
context') added an optional jobId field on PluginHookAgentContext,
populated for cron-driven runs. The commit shipped without a docs
update or CHANGELOG entry, so plugin authors had no visible signal
that the new ctx.jobId field exists.

Surface ctx.jobId in two existing hook context references in
docs/plugins/hooks.md: the before_tool_call ctx-fields list, and the
runId/agent-lifecycle paragraph that already names ctx.runId — extend
it to note ctx.jobId on cron-driven runs and what plugins can do with
it (scope metrics, side effects, or state to a scheduled job).
2026-04-25 18:38:27 -07:00
Peter Steinberger
8ed52c1463 fix: bound configured acp binding readiness 2026-04-26 02:36:58 +01:00
5702 changed files with 272677 additions and 70039 deletions

View File

@@ -16,6 +16,19 @@ warm caches, local build state, and fast feedback.
Testbox is the expensive path. Reach for it deliberately.
OpenClaw maintainers can opt into Testbox-first validation by setting
`OPENCLAW_TESTBOX=1` in their environment or standing agent rules. This mode is
maintainers-only and requires Blacksmith access.
When `OPENCLAW_TESTBOX=1` is set in OpenClaw:
- Pre-warm a Testbox early for longer, wider, or uncertain work.
- Prefer Testbox for `pnpm` gates, e2e, package-like proof, and broad suites.
- Reuse the same Testbox ID for every run command in the same task/session.
- Use local commands only when the task explicitly sets
`OPENCLAW_LOCAL_CHECK_MODE=throttled|full`, or when the user asks for local
proof.
## Install the CLI
If `blacksmith` is not installed, install it:
@@ -81,7 +94,8 @@ Prefer Testbox when:
- you are reproducing CI-only failures
- you need the exact workflow image/job environment from GitHub Actions
For OpenClaw specifically, normal local iteration should stay local:
For OpenClaw specifically, normal local iteration stays local unless maintainer
Testbox mode is enabled with `OPENCLAW_TESTBOX=1`:
- `pnpm check:changed`
- `pnpm test:changed`
@@ -89,27 +103,49 @@ For OpenClaw specifically, normal local iteration should stay local:
- `pnpm test:serial`
- `pnpm build`
Only use Testbox in OpenClaw when the user explicitly wants CI-parity or the
check truly depends on remote secrets/services that the local repo loop cannot
provide.
If `OPENCLAW_TESTBOX=1` is enabled, run those same repo commands inside the
warm Testbox. If the user wants laptop-friendly local proof for one command, use
the explicit escape hatch `OPENCLAW_LOCAL_CHECK_MODE=throttled`.
For installable-package product proof, prefer the GitHub `Package Acceptance`
workflow over an ad hoc Testbox command. It resolves one package candidate
(`source=npm`, `source=ref`, `source=url`, or `source=artifact`), uploads it as
`package-under-test`, and runs the reusable Docker E2E lanes against that exact
tarball on GitHub/Blacksmith runners. Use `workflow_ref` for the trusted
workflow/harness code and `package_ref` for the source ref to pack when testing
an older trusted branch, tag, or SHA.
## Setup: Warmup before coding
If you decided Testbox is actually warranted, warm one up early. This returns
an ID instantly and boots the CI environment in the background while you work:
If you decided Testbox is warranted, warm one up early. This returns an ID
instantly and boots the CI environment in the background while you work:
blacksmith testbox warmup ci-check-testbox.yml
# → tbx_01jkz5b3t9...
Save this ID. You need it for every `run` command.
For OpenClaw maintainer Testbox mode, pre-warm at the start of longer or wider
tasks:
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
Use the build-artifact warmup when e2e/package/build proof benefits from seeded
`dist/`, `dist-runtime/`, and build-all caches:
blacksmith testbox warmup ci-build-artifacts-testbox.yml --ref main --idle-timeout 90
Warmup dispatches a GitHub Actions workflow that provisions a VM with the
full CI environment: dependencies installed, services started, secrets
injected, and a clean checkout of the repo at the default branch.
In OpenClaw, raw commit SHAs are not reliable dispatch refs for `warmup --ref`;
use a branch or tag. The build-artifact workflow resolves `openclaw@beta` and
`openclaw@latest` to SHA cache keys internally.
Options:
--ref <branch> Git ref to dispatch against (default: repo's default branch)
--ref <branch|tag> Git ref to dispatch against (default: repo's default branch)
--job <name> Specific job within the workflow (if it has multiple)
--idle-timeout <min> Idle timeout in minutes (default: 30)
@@ -226,6 +262,11 @@ services, CI-only runners, or reproducibility against the workflow image.
If the repo says local tests/builds are the normal path, follow the repo.
OpenClaw maintainer exception: if `OPENCLAW_TESTBOX=1` is set by the user or
agent environment, treat Testbox as the normal validation path for this repo.
Use `OPENCLAW_LOCAL_CHECK_MODE=throttled|full` as the explicit local escape
hatch.
## When to use
Use Testbox when:
@@ -242,18 +283,25 @@ checks that need parity or remote state.
## Workflow
1. Decide whether the repo's local loop is the right default.
2. Only if Testbox is warranted, warm up early:
`blacksmith testbox warmup ci-check-testbox.yml` → save the ID
1. Decide whether the repo's local loop is the right default. For OpenClaw,
`OPENCLAW_TESTBOX=1` makes Testbox the maintainer default.
2. If Testbox is warranted, warm up early:
`blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90` → save the ID
3. Write code while the testbox boots in the background.
4. Run the remote command when needed:
`blacksmith testbox run --id <ID> "npm test"`
`blacksmith testbox run --id <ID> "pnpm check:changed"`
5. If tests fail, fix code and re-run against the same warm box.
6. If you changed dependency manifests (package.json, etc.), prepend
the install command: `blacksmith testbox run --id <ID> "npm install && npm test"`
7. If you need artifacts (coverage reports, build outputs, etc.), download them:
7. If a narrow PR reports a full sync or the box was reused/expired, sanity
check the remote copy before a slow gate:
`blacksmith testbox run --id <ID> "pnpm testbox:sanity"`.
If it reports missing root files or mass tracked deletions, stop the box and
warm a fresh one. Use `OPENCLAW_TESTBOX_ALLOW_MASS_DELETIONS=1` only for an
intentional large deletion PR.
8. If you need artifacts (coverage reports, build outputs, etc.), download them:
`blacksmith testbox download --id <ID> coverage/ ./coverage/`
8. Once green, commit and push.
9. Once green, commit and push.
## OpenClaw full test suite
@@ -268,9 +316,15 @@ Observed full-suite time on Blacksmith Testbox is about 3-4 minutes:
- 173-180s on a warmed box
- 219s on a fresh 32-vCPU box
When validating before commit/push, run `pnpm check:changed` first when
appropriate, then the full suite with the profile above if broad confidence is
needed.
When validating before commit/push in maintainer Testbox mode, run
`pnpm check:changed` inside the warmed box first when appropriate, then the full
suite with the profile above if broad confidence is needed.
Run `pnpm testbox:sanity` inside the warmed box before the broad command when
the sync looks suspicious. It checks that root files such as `pnpm-lock.yaml`
still exist and fails on 200 or more tracked deletions. That catches stale or
corrupted rsync state before dependency install or Vitest failures hide the real
problem.
## Examples
@@ -324,12 +378,14 @@ timeout is reached). Default timeout is 5m; use `--wait-timeout` for longer
blacksmith testbox stop --id <ID>
Testboxes automatically shut down after being idle (default: 30 minutes).
If you need a longer session, increase the timeout at warmup time:
If you need a longer session, increase the timeout at warmup time. For OpenClaw
maintainer work, use 90 minutes for long-running sessions:
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 60
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 90
blacksmith testbox warmup ci-build-artifacts-testbox.yml --idle-timeout 90
## With options
blacksmith testbox warmup ci-check-testbox.yml --ref main
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 60
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 90
blacksmith testbox run --id <ID> "go test ./..."

View File

@@ -0,0 +1,37 @@
---
name: discord-clawd
description: Use to talk to the Discord-backed OpenClaw agent/session; not for archive search.
---
# Discord Clawd
Use this when the task is to talk with the Discord-backed agent/session, ask it a question, or post through that route.
For Discord archive/history/search, use `$discrawl` instead.
## Transport
Use the OpenClaw relay helper:
```bash
cd ~/Projects/agent-scripts
python3 skills/openclaw-relay/scripts/openclaw_relay.py targets
python3 skills/openclaw-relay/scripts/openclaw_relay.py resolve --target maintainers
```
If the target alias exists, prefer a private ask first:
```bash
python3 skills/openclaw-relay/scripts/openclaw_relay.py ask \
--target maintainers \
--message "Reply with exactly OK."
```
Use `publish` when the session should decide whether to post. Use `force-send` only when the user explicitly wants a message posted.
## Guardrails
- Resolve the target before sending real content.
- Report the target and delivery mode used.
- Do not use this for local Discord archive queries.
- Do not expose gateway tokens or session secrets.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Discord Clawd"
short_description: "Talk to the Discord-backed OpenClaw agent"
default_prompt: "Use $discord-clawd to route a private ask or explicit post through the Discord-backed OpenClaw agent/session."

View File

@@ -0,0 +1,68 @@
---
name: gitcrawl
description: Use gitcrawl for OpenClaw issue and PR archive search, duplicate discovery, related-thread clustering, and local GitHub mirror freshness checks.
metadata:
openclaw:
requires:
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:
```bash
gitcrawl doctor --json
```
2. Read the target from the local archive:
```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
```
4. Inspect relevant clusters:
```bash
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
5. Verify anything actionable with live GitHub and the checkout:
```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.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Gitcrawl"
short_description: "Search local OpenClaw issue and PR history before live GitHub triage"
default_prompt: "Use $gitcrawl to inspect OpenClaw issue and PR history, find related threads and duplicate candidates, then verify actionable decisions with live GitHub."

View File

@@ -7,6 +7,23 @@ description: Review, triage, close, label, comment on, or land OpenClaw PRs/issu
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Start issue and PR triage with gitcrawl
- Use `$gitcrawl` first anytime you inspect OpenClaw issues or PRs.
- Check local `gitcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
- Use `gitcrawl` for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
- If `gitcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `gitcrawl sync --include-comments`, future enrichment commands, or broad reclustering unless the user asked to update the local store or stale data is blocking the decision.
Common read-only path:
```bash
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
gitcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --json
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
## Apply close and triage labels correctly
- If an issue or PR matches an auto-close reason, apply the label and let `.github/workflows/auto-response.yml` handle the comment/close/lock flow.
@@ -59,9 +76,9 @@ Use this skill for maintainer-facing GitHub workflow, not for ordinary code chan
## Search broadly before deciding
- Prefer targeted keyword search before proposing new work or closing something as duplicate.
- Use `--repo openclaw/openclaw` with `--match title,body` first.
- Add `--match comments` when triaging follow-up discussion.
- Prefer `gitcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Use `--repo openclaw/openclaw` with `--match title,body` first when using `gh search`.
- Add `--match comments` when triaging follow-up discussion or closed-as-duplicate chains.
- Do not stop at the first 500 results when the task requires a full search.
Examples:

View File

@@ -49,6 +49,37 @@ pnpm openclaw qa suite \
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## OTEL smoke
For local QA-lab OpenTelemetry validation, use:
```bash
pnpm qa:otel:smoke
```
This starts a local OTLP/HTTP trace receiver, runs the `otel-trace-smoke`
scenario through qa-channel, decodes the emitted protobuf spans, and verifies
the exported trace names and privacy contract. It does not require Opik,
Langfuse, or external collector credentials.
## Matrix live profiles
`pnpm openclaw qa matrix` defaults to the full `all` profile. Use explicit
profiles for faster CI/release proof:
```bash
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000 \
pnpm openclaw qa matrix --profile fast --fail-fast
```
- `fast`: release-critical transport contract, excluding generated image and
deep E2EE recovery inventory.
- `transport`, `media`, `e2ee-smoke`, `e2ee-deep`, `e2ee-cli`: sharded full
Matrix coverage.
- `QA-Lab - All Lanes` uses explicit `fast` Matrix on scheduled runs. Manual
dispatch keeps `matrix_profile=all` as the default and always shards that full
Matrix selection.
## QA credentials and 1Password
- Use `op` only inside `tmux` for QA secret lookup in this repo.

View File

@@ -25,15 +25,36 @@ Use this skill for release and publish-time workflow. Keep ordinary development
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
changelog rewrite immediately before creating the release branch.
- During release planning, inspect both `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
before final publish. For every deprecated or removal-pending compatibility
record whose `removeAfter` date is on or before the release date, either
remove the compatibility path where safe and validate the affected tests, or
write down why removal is blocked and get explicit maintainer approval before
shipping the expired compatibility path.
- When removing deprecated runtime/config compatibility, preserve any doctor
migration, repair, or hint that is still needed by supported upgrade paths.
Doctor-side compatibility should stay tracked in
`src/commands/doctor/shared/deprecation-compat.ts` until maintainers confirm
the repair is no longer needed.
- Revalidate compatibility replacement text during release planning. The
recommended replacement can shift as plugin ownership, externalization, and
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after they leave the machine. If a
published or pushed beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, run the full pre-npm test roster before publishing
each beta. After a beta is published, run the smaller published-install roster
focused on install/update/Docker/Parallels. If anything fails, fix it on the
release branch, commit/push/pull, increment beta number, and repeat. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
the release branch, commit/push/pull, increment beta number, and repeat. Run
the full expensive roster at least once before stable/latest promotion; for
later beta attempts, rerun only lanes whose evidence changed unless the fix
touches broad release, install/update, plugin, Docker, Parallels, or live QA
behavior. After each beta is published, scan current `main` once for critical
fixes that landed after the release branch cut and backport only important
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
after 4 failed beta attempts, stop and report.
- Use `/changelog` before version/tag preparation so the top changelog section
is deduped and ordered by user impact.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
@@ -75,6 +96,11 @@ Use this skill for release and publish-time workflow. Keep ordinary development
parallel, publish npm from the successful npm preflight, then start published
npm install/update, Docker, and Parallels verification while mac artifacts
continue.
- After a beta is published, overlap remote/manual release rosters where useful,
but avoid piling local Docker, Parallels, and QA-Lab work onto the same host
when it would create system-load noise. Use selective reruns after failures or
fixes, but keep proof that Docker, Parallels, and QA-Lab each passed at least
once before stable/latest promotion.
- Mac packaging may be built from a slight release-branch variation of the
tagged commit when the delta is mac packaging, signing, workflow, or
validation-only release machinery. If mac packaging needs release-branch-only
@@ -107,6 +133,13 @@ Use this skill for release and publish-time workflow. Keep ordinary development
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.D` through the line before the
next level-2 heading and use that complete block as the release notes.
- When preparing release notes, scan `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` for compatibility records
with `warningStarts` or `removeAfter` within 7 days after the release date.
Add an `Upcoming deprecations` note to the release notes when any exist,
including the compatibility code, target date, replacement, and a link to the
record's `docsPath` or `/plugins/compatibility` when no more specific
deprecation page exists.
- When cutting a mac release with a beta GitHub prerelease:
- tag `vYYYY.M.D-beta.N` from the release commit
- create a prerelease titled `openclaw YYYY.M.D-beta.N`
@@ -148,6 +181,9 @@ live`; keep it clearly beta and avoid implying stable promotion.
compact launch post, then publish one focused feature explainer per reply.
Follow-up replies should not repeat "new in VERSION" or the version number
when the thread context already makes it obvious.
- Peter's preferred thread workflow: first agree on the generic launch tweet,
then proceed through follow-up tweets one by one. When he says `next`, provide
or copy the next follow-up only; do not dump the full thread again unless asked.
- Every follow-up tweet should include a docs URL for that specific feature.
Prefer a bare URL over `Docs: <url>` unless the label is needed for clarity.
Keep follow-ups concise: around 160-220 raw characters is usually the sweet
@@ -202,10 +238,16 @@ Before tagging or publishing, run:
pnpm check:architecture
pnpm build
pnpm ui:build
pnpm qa:otel:smoke
pnpm release:check
pnpm test:install:smoke
```
- Use `pnpm qa:otel:smoke` when release validation needs telemetry coverage.
It starts a local OTLP/HTTP trace receiver, runs QA-lab's
`otel-trace-smoke`, and checks span names plus content/identifier redaction
without external Opik or Langfuse credentials.
For a non-root smoke path:
```bash
@@ -286,9 +328,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Docker install/update coverage that exercises the published beta package
- published npm Telegram proof: dispatch Actions > `NPM Telegram Beta E2E`
from `main` with `package_spec=openclaw@<beta-version>` and
`provider_mode=mock-openai`, approve `npm-release`, and require success.
This is the default button path for installed-package onboarding,
Telegram setup, and real Telegram E2E against the published npm package.
`provider_mode=mock-openai`, and require success. This workflow is
maintainer-dispatched and intentionally has no `npm-release` approval gate;
`qa-live-shared` only supplies the shared QA secrets. This is the default
button path for installed-package onboarding, Telegram setup, and real
Telegram E2E against the published npm package.
Use the local `pnpm test:docker:npm-telegram-live` lane with the matching
`OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC` and Convex CI env only as a fallback
or debugging path.
@@ -485,8 +529,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
6. Create `release/YYYY.M.D` from that post-changelog `main` commit.
7. Make every repo version location match the beta tag before creating it.
8. Commit release preparation changes on the release branch and push the branch.
9. Run the local build, Docker, and Parallels parts of the full pre-npm beta
test roster from the release branch before any npm preflight or publish.
9. Run the fast local beta preflight from the release branch before any npm
preflight or publish. Keep expensive Docker, Parallels, and published-package
install/update lanes for after the beta is live unless the operator asks to
run them before beta publication.
10. For beta releases, skip mac app build/sign/notarize unless beta scope or a
release blocker specifically requires it. For stable releases, include the
mac app, signing, notarization, and appcast path.
@@ -523,10 +569,16 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
21. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
22. Run postpublish verification:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
23. Run the post-published beta verification roster. If any lane fails after
the beta tag/package is pushed or published, fix, commit/push/pull,
increment to the next beta tag, and restart at the full pre-npm beta test
roster for the new beta. The roster includes the manual Actions >
23. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to
the next beta if the fix must change the already-published package. If any
lane fails after the beta tag/package is pushed or published, fix,
commit/push/pull, increment to the next beta tag, and rerun the affected
beta evidence. Once the beta is live, start remote/manual rosters where they
can overlap safely, but keep local Docker and Parallels load controlled.
Ensure the full expensive roster has passed at least once before
stable/latest promotion. The roster includes the manual Actions >
`NPM Telegram Beta E2E` workflow against the exact published beta package.
If a pre-npm lane fails before any tag/package leaves the machine, fix and
rerun the same intended beta attempt. Repeat up to the operator's

View File

@@ -0,0 +1,603 @@
---
name: openclaw-testing
description: Choose, run, rerun, or debug OpenClaw tests, CI checks, Docker E2E lanes, release validation, and the cheapest safe verification path.
---
# OpenClaw Testing
Use this skill when deciding what to test, debugging failures, rerunning CI,
or validating a change without wasting hours.
## Read First
- `docs/reference/test.md` for local test commands.
- `docs/ci.md` for CI scope, release checks, Docker chunks, and runner behavior.
- Scoped `AGENTS.md` files before editing code under a subtree.
## Default Rule
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- source: `pnpm changed:lanes --json`, then `pnpm check:changed`
- tests only: `pnpm test:changed`
- one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- 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.
3. Fix root cause.
4. Rerun the same narrow proof.
5. Broaden only when the touched contract demands it.
## Guardrails
- Do not kill unrelated processes or tests. If something is running elsewhere, treat it as owned by the user or another agent.
- Do not run expensive local Docker, full release checks, full `pnpm test`, or full `pnpm check` unless the user asks or the change genuinely requires it.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
## Local Test Shortcuts
```bash
pnpm changed:lanes --json
pnpm check:changed # changed typecheck/lint/guards; no Vitest
pnpm test:changed # cheap smart changed Vitest targets
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed
pnpm test <path-or-filter> -- --reporter=verbose
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
```
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
`pnpm test` wrapper so project routing, workers, and setup stay correct.
## Command Semantics
- `pnpm check` and `pnpm check:changed` do not run Vitest tests. They are for
typecheck, lint, and guard proof.
- `pnpm test` and `pnpm test:changed` run Vitest tests.
- `pnpm test:changed` is intentionally cheap by default: direct test edits,
sibling tests, explicit source mappings, and import-graph dependents.
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` is the explicit broad
fallback for harness/config/package edits that genuinely need it.
- Do not run extension sweeps just because core changed. If a core edit is for a
specific plugin bug, run that plugin's tests explicitly. If a public SDK or
contract change needs consumer proof, choose the smallest representative
plugin/contract tests first, then broaden only when the risk justifies it.
- The test wrapper prints a short `[test] passed|failed|skipped ... in ...`
line. Vitest's own duration is still the per-shard detail.
## Routing Model
- `pnpm changed:lanes --json` answers "which check lanes does this diff touch?"
It is used by `pnpm check:changed` for typecheck/lint/guard selection.
- `pnpm test:changed` answers "which Vitest targets are worth running now?" It
uses the same changed path list, but applies a cheaper test-target resolver.
- Direct test edits run themselves. Source edits prefer explicit mappings,
sibling `*.test.ts`, then import-graph dependents. Shared harness/config/root
edits are skipped by default unless they have precise mapped tests.
- Shared group-room delivery config and source-reply prompt edits are precise
mapped tests: they run the core auto-reply regressions plus Discord and Slack
delivery tests so cross-channel default changes fail before a PR push.
- Public SDK or contract edits do not automatically run every plugin test.
`check:changed` proves extension type contracts; the agent chooses the
smallest plugin/contract Vitest proof that matches the actual risk.
- Use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when a harness,
config, package, or unknown-root edit really needs the broad Vitest fallback.
## CI Debugging
Start with current run state, not logs for everything:
```bash
gh run list --branch main --limit 10
gh run view <run-id> --json status,conclusion,headSha,url,jobs
gh run view <run-id> --job <job-id> --log
```
- Check exact SHA. Ignore newer unrelated `main` unless asked.
- For cancelled same-branch runs, confirm whether a newer run superseded it.
- Fetch full logs only for failed or relevant jobs.
## GitHub Release Workflows
Use the smallest workflow that proves the current risk. The full umbrella is
available, but it is usually the last step after narrower proof, not the first
rerun after a focused patch.
### Full Release Validation
`Full Release Validation` (`.github/workflows/full-release-validation.yml`) is
the manual "everything before release" umbrella. It resolves a target ref, then
dispatches:
- manual `CI` for the full normal CI graph
- `OpenClaw Release Checks` for install smoke, cross-OS release checks, live and
E2E checks, Docker release-path suites, OpenWebUI, QA Lab, fast Matrix, and
Telegram release lanes
- optional post-publish Telegram E2E when a package spec is supplied
Run it only when validating an actual release candidate, after broad shared CI
or release orchestration changes, or when explicitly asked:
```bash
gh workflow run full-release-validation.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=stable
```
Run the workflow itself from the trusted current ref, normally `--ref main`;
child workflows are dispatched from that same ref even when `ref` points at an
older release branch or tag. Full Release Validation has no separate child
workflow ref input; choose the trusted harness by choosing the workflow run ref.
Use `release_profile=minimum|stable|full` to control live/provider breadth:
`minimum` keeps the fastest OpenAI/core release-critical set, `stable` adds the
stable provider/backend set, and `full` adds the broad advisory provider/media
matrix. Do not make `full` faster by silently dropping suites; optimize setup,
artifact reuse, and sharding instead. The parent verifier job appends
slowest-job tables for child runs; rerun only that verifier after a child rerun
turns green.
If a full run is already active on a newer `origin/main`, prefer watching that
run over dispatching a duplicate. If you accidentally dispatch a stale duplicate,
cancel it and monitor the current run.
The child-dispatch jobs record the child run ids. The final
`Verify full validation` job re-queries those child runs and is the canonical
parent gate. If a child workflow failed but was later rerun successfully, rerun
only the failed parent verifier job; do not dispatch a new full umbrella unless
the release evidence is stale.
For bounded recovery after a focused fix, pass `-f rerun_group=<group>`.
Supported umbrella groups are `all`, `ci`, `release-checks`, `install-smoke`,
`cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and
`npm-telegram`. Use the narrowest group that covers the failed box.
### Release Evidence
After release-candidate validation or before a release decision, record the
important run ids in the private `openclaw/releases-private` evidence ledger.
Use the manual `OpenClaw Release Evidence`
(`openclaw-release-evidence.yml`) workflow there. It writes durable summaries
under `evidence/<release-id>/` and commits:
- `release-evidence.md`
- `release-evidence.json`
- `index.json`
- `runs/<label>.json`
Use one run per line:
```text
full-release-validation openclaw/openclaw <run-id> blocking
package-acceptance openclaw/openclaw <run-id> blocking
release-checks openclaw/openclaw <run-id> blocking
```
Store summaries, run URLs, artifact metadata, timings, pass/fail state, and
short release-manager notes there. Do not store raw logs, provider
prompts/responses, channel transcripts, signing material, or secret-bearing
config in git; raw logs stay in Actions artifacts.
When `Full Release Validation` completes and
`OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN` is configured in the public repo, it
requests the private `OpenClaw Release Evidence From Full Validation` workflow.
That private workflow reads the parent full-validation run, extracts the child
CI/release-checks/Telegram run ids from the parent logs, and opens the evidence
PR automatically. If the token is absent or the run predates this wiring, trigger
that private workflow manually with the full-validation run id.
### Release Checks
`OpenClaw Release Checks` (`openclaw-release-checks.yml`) is the release child
workflow. It is broader than normal CI but narrower than the umbrella because it
does not dispatch the separate full normal CI child. It runs Package Acceptance
with artifact-native delta lanes and `telegram_mode=mock-openai`, so the release
package tarball also goes through offline plugin proof, bundled-channel compat,
and Telegram package QA. The Docker release-path chunks cover the overlapping
package/update/plugin lanes. Use it when release-path validation is needed
without rerunning the entire umbrella.
```bash
gh workflow run openclaw-release-checks.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=stable \
-f rerun_group=all
```
Release-check rerun groups are `all`, `install-smoke`, `cross-os`, `live-e2e`,
`package`, `qa`, `qa-parity`, and `qa-live`.
`OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected
ref once as `release-package-under-test` and passes that artifact into cross-OS
release checks, release-path Docker live/E2E checks, and Package Acceptance.
When `Full Release Validation` dispatches release checks, it passes the requested
branch/tag plus an `expected_sha` so branch/tag refs resolve through the fast
remote-ref path while the package and QA jobs still validate the exact SHA.
The release Docker path intentionally shards the plugin/runtime tail. The
workflow uses `plugins-runtime-plugins`, `plugins-runtime-services`, and
`plugins-runtime-install-a` through `plugins-runtime-install-d`; aggregate
aliases such as `plugins-runtime-core`, `plugins-runtime`, and
`plugins-integrations` remain for manual reruns.
The release QA parity box is internally split into candidate and baseline lane
jobs, followed by a report job that downloads both artifacts and runs
`pnpm openclaw qa parity-report`. For parity failures, inspect the failed lane
first; inspect the report job when both lane summaries exist but the comparison
fails.
### QA Lab Matrix Profiles
`pnpm openclaw qa matrix` defaults to `--profile all`. Do not assume the CLI
default is the fast release path. Use explicit profiles:
- `--profile fast`: release-critical Matrix transport contract; add
`--fail-fast` only when the target CLI supports it
- `--profile transport|media|e2ee-smoke|e2ee-deep|e2ee-cli`: sharded full
Matrix proof
- `OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000`: CI-friendly no-reply quiet
window when paired with fast or sharded gates
`QA-Lab - All Lanes` uses explicit fast Matrix on scheduled runs; manual
dispatch keeps `matrix_profile=all` as the default and always shards that full
Matrix selection. `OpenClaw Release Checks` uses explicit fast Matrix; run the
all-lanes workflow when release investigation needs full Matrix media/E2EE
inventory.
### Reusable Live/E2E Checks
`OpenClaw Live And E2E Checks (Reusable)`
(`openclaw-live-and-e2e-checks-reusable.yml`) is the preferred entry point for
targeted live, Docker, model, and E2E proof. Inputs let you turn off unrelated
lanes:
```bash
gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<sha> \
-f include_repo_e2e=false \
-f include_release_path_suites=false \
-f include_openwebui=false \
-f include_live_suites=true \
-f live_models_only=true \
-f live_model_providers=fireworks
```
Useful knobs:
- `docker_lanes='<lane[,lane]>'`: run selected Docker scheduler lanes against
prepared artifacts instead of the release chunk matrix. Multiple selected
lanes fan out as parallel targeted Docker jobs after one shared package/image
preparation step.
- `include_live_suites=false`: skip live/provider suites when testing Docker
scheduler or release packaging only.
- `live_models_only=true`: run only Docker live model coverage.
- `live_model_providers=fireworks` (or comma/space separated providers): run one
targeted Docker live model job instead of the full provider matrix.
- blank `live_model_providers`: run the full live-model provider matrix.
Release-path Docker chunks are currently `core`, `package-update-openai`,
`package-update-anthropic`, `package-update-core`,
`plugins-runtime-plugins`, `plugins-runtime-services`,
`plugins-runtime-install-a`, `plugins-runtime-install-b`,
`plugins-runtime-install-c`, `plugins-runtime-install-d`,
`bundled-channels-core`, `bundled-channels-update-a`,
`bundled-channels-update-b`, and `bundled-channels-contracts`. The aggregate
`bundled-channels`, `plugins-runtime-core`, `plugins-runtime`, and
`plugins-integrations` chunks remain valid for manual one-shot reruns, but
release checks use the split chunks.
When live suites are enabled, the workflow shards broad native `pnpm test:live`
coverage through `scripts/test-live-shard.mjs` instead of one serial `live-all`
job:
- `native-live-src-agents`
- `native-live-src-gateway-core`
- `native-live-src-gateway-profiles` (release CI runs this with provider
filters such as `OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic`)
- `native-live-src-gateway-backends`
- `native-live-test`
- `native-live-extensions-a-k`
- `native-live-extensions-l-n`
- `native-live-extensions-openai`
- `native-live-extensions-o-z`
- `native-live-extensions-o-z-other`
- `native-live-extensions-xai`
- `native-live-extensions-media`
- `native-live-extensions-media-audio`
- `native-live-extensions-media-music`
- `native-live-extensions-media-music-google`
- `native-live-extensions-media-music-minimax`
- `native-live-extensions-media-video`
Use `node scripts/test-live-shard.mjs <shard> --list` to see the exact files
before rerunning a failed native live shard. The aggregate `o-z` and `media`
shards remain useful locally; release CI uses the smaller provider/media shards
so one live-provider flake does not force a broad native live rerun.
For model-list or provider-selection fixes, use `live_models_only=true` plus the
specific `live_model_providers` allowlist. Confirm logs show the expected
`OPENCLAW_LIVE_PROVIDERS` and selected model ids before declaring proof.
## Docker
Docker is expensive. First inspect the scheduler without running Docker:
```bash
OPENCLAW_DOCKER_ALL_DRY_RUN=1 pnpm test:docker:all
OPENCLAW_DOCKER_ALL_DRY_RUN=1 OPENCLAW_DOCKER_ALL_LANES=install-e2e pnpm test:docker:all
OPENCLAW_DOCKER_ALL_LANES=install-e2e node scripts/test-docker-all.mjs --plan-json
```
Run one failed lane locally only when explicitly asked or when GitHub is not
usable:
```bash
OPENCLAW_DOCKER_ALL_LANES=<lane> \
OPENCLAW_DOCKER_ALL_BUILD=0 \
OPENCLAW_DOCKER_ALL_PREFLIGHT=0 \
OPENCLAW_SKIP_DOCKER_BUILD=1 \
OPENCLAW_DOCKER_E2E_BARE_IMAGE='<prepared-bare-image>' \
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE='<prepared-functional-image>' \
pnpm test:docker:all
```
For release validation, prefer the reusable GitHub workflow input:
```yaml
docker_lanes: install-e2e
```
Multiple lanes are allowed:
```yaml
docker_lanes: install-e2e bundled-channel-update-acpx
```
That skips the release chunk matrix and runs one targeted Docker job against the
prepared GHCR images and the selected package artifact. Rerun commands
generated inside GitHub artifacts include `package_artifact_run_id`,
`package_artifact_name`, `docker_e2e_bare_image`, and
`docker_e2e_functional_image` when available, so failed lanes can reuse the
exact tarball and prepared images from the failed run. When the fix changes
package contents, omit those reuse inputs so the workflow packs a new tarball.
Live-only targeted reruns skip the E2E images and build only the live-test
image. Release-path normal mode fans out into smaller Docker chunk jobs:
- `core`
- `package-update-openai`
- `package-update-anthropic`
- `package-update-core`
- `plugins-runtime-plugins`
- `plugins-runtime-services`
- `plugins-runtime-install-a`
- `plugins-runtime-install-b`
- `plugins-runtime-install-c`
- `plugins-runtime-install-d`
- `bundled-channels`
OpenWebUI is folded into `plugins-runtime-services` for full release-path
coverage and keeps a standalone `openwebui` chunk only for OpenWebUI-only
dispatches. The legacy `package-update`, `plugins-runtime-core`,
`plugins-runtime`, and `plugins-integrations` chunks still work as aggregate
aliases for manual reruns, but the release workflow uses the split chunks so
provider installer checks, plugin runtime checks, bundled plugin
install/uninstall shards, and bundled-channel checks can run on separate
machines. The bundled-channel runtime-dependency coverage
inside `bundled-channels`
uses the split `bundled-channel-*` and `bundled-channel-update-*` lanes rather
than the serial `bundled-channel-deps` lane, so failures produce cheap targeted
reruns for the exact channel/update scenario. The bundled plugin
install/uninstall sweep is also split into
`bundled-plugin-install-uninstall-0` through
`bundled-plugin-install-uninstall-7`; selecting the legacy
`bundled-plugin-install-uninstall` lane expands to all eight shards.
## Package Acceptance
Use the manual `Package Acceptance` workflow when the question is "does this
installable package work as a product?" rather than "does this source diff pass
Vitest?"
In release validation, treat Package Acceptance as the package-candidate shard
inside the larger release umbrella, not as a competing full-test path. Full
Release Validation and private release gauntlets should call Package Acceptance
for tarball resolution, Docker product/package proof, and optional Telegram QA
against the same resolved `package-under-test` artifact; keep orchestration,
secret policy, blocking/advisory status, and evidence rollup in the caller.
Good defaults:
```bash
gh workflow run package-acceptance.yml --ref main \
-f source=npm \
-f workflow_ref=main \
-f package_spec=openclaw@beta \
-f suite_profile=product \
-f telegram_mode=mock-openai
```
Npm candidate selection:
- Resolve the registry immediately before dispatch:
`npm view openclaw dist-tags --json --prefer-online --cache /tmp/openclaw-npm-cache-verify-$$`
and `npm view openclaw@beta version dist.tarball dist.integrity --json --prefer-online --cache /tmp/openclaw-npm-cache-verify-$$`.
- If Peter asks for "latest beta", use `source=npm` with
`package_spec=openclaw@beta`, then record the resolved version from `npm view`
or the workflow summary.
- For reruns, release proof, or comparing one known package, prefer the exact
immutable spec: `package_spec=openclaw@YYYY.M.D-beta.N` or
`package_spec=openclaw@YYYY.M.D`.
- For stable package proof, use `package_spec=openclaw@latest` only when the
question is explicitly the current stable dist-tag; otherwise pin the exact
version.
- `source=npm` only accepts registry specs for `openclaw@beta`,
`openclaw@latest`, or exact OpenClaw release versions. Do not pass semver
ranges, git refs, file paths, tarball URLs, or plugin package names there.
- If the candidate is a tarball URL, use `source=url` with `package_sha256`. If
it is an Actions tarball artifact, use `source=artifact`. If it is an
unpublished source candidate, use `source=ref` with a trusted ref or SHA.
- Package acceptance tests exactly the selected package candidate. Do not apply
`openclaw update --channel beta` fallback semantics here; if `beta` is absent,
stale, older than `latest`, or points at a broken tarball, report that tag
state instead of silently testing `latest`.
Profiles:
- `smoke`: quick confidence that the tarball installs, can onboard a channel,
can run an agent turn, and basic gateway/config lanes work.
- `package`: release-package contract. Adds installer/update, doctor install
switching, bundled plugin runtime deps, plugin install/update, and package
repair lanes. This is the default native replacement for most Parallels
package/update coverage.
- `product`: package profile plus broader product surfaces: MCP channels,
cron/subagent cleanup, OpenAI web search, and OpenWebUI.
- `full`: split Docker release-path chunks with OpenWebUI.
- `custom`: exact `docker_lanes` list for a focused rerun.
Candidate sources:
- `source=npm`: `openclaw@beta`, `openclaw@latest`, or an exact release version.
- `source=ref`: pack `package_ref` using the trusted `workflow_ref` harness.
This intentionally separates old package commits from new workflow/test code.
- `source=url`: HTTPS `.tgz` plus required `package_sha256`.
- `source=artifact`: download one `.tgz` from `artifact_run_id`/`artifact_name`.
Ref model:
- `gh workflow run ... --ref <workflow-ref>` selects the workflow file revision
GitHub executes.
- `workflow_ref` is the trusted harness/script ref passed to reusable Docker
E2E.
- `package_ref` is the source ref to build when `source=ref`. It can be an
older branch/tag/SHA as long as it is reachable from an OpenClaw branch or
release tag.
Example: run latest package acceptance harness against an older trusted commit:
```bash
gh workflow run package-acceptance.yml --ref main \
-f workflow_ref=main \
-f source=ref \
-f package_ref=<branch-or-sha> \
-f suite_profile=package \
-f telegram_mode=mock-openai
```
Use `telegram_mode=mock-openai` or `telegram_mode=live-frontier` when the same
resolved `package-under-test` tarball should also run through the Telegram QA
workflow in the `qa-live-shared` environment. The standalone Telegram workflow
still accepts a published npm spec for post-publish checks, but Package
Acceptance passes the resolved artifact for `source=npm`, `ref`, `url`, and
`artifact`. Use `telegram_mode=none` only when intentionally skipping Telegram
credentialed package proof for a focused rerun.
Docker E2E images never copy repo sources as the app under test: the bare image
is a Node/Git runner, and the functional image installs the same prebuilt npm
tarball that bare lanes mount. `scripts/package-openclaw-for-docker.mjs` is the
single packer for local scripts and CI and validates the tarball inventory
before Docker consumes it. `scripts/test-docker-all.mjs --plan-json` is the
scheduler-owned CI plan for image kind, package, live image, lane, and
credential needs. Docker lane definitions live in the single scenario catalog
`scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in
`scripts/lib/docker-e2e-plan.mjs`. `scripts/docker-e2e.mjs` converts plan and
summary JSON into GitHub outputs and step summaries. Every scheduler run writes
`.artifacts/docker-tests/**/summary.json` plus `failures.json`. Read those
before rerunning. Lane entries include `command`, `rerunCommand`, status,
timing, timeout state, image kind, and log file path. The summary also includes
top-level phase timings for preflight, image build, package prep, lane pools,
and cleanup. Use `pnpm test:docker:timings <summary.json>` to rank slow lanes
and phases before deciding whether a broader rerun is justified.
## Cheap Docker Reruns
First derive the smallest rerun command from artifacts:
```bash
pnpm test:docker:rerun <github-run-id>
pnpm test:docker:rerun .artifacts/docker-tests/<run>/failures.json
```
The script downloads Docker E2E artifacts for a GitHub run, reads
`summary.json`/`failures.json`, and prints a combined targeted workflow command
plus per-lane commands. Prefer the combined targeted command when several lanes
failed for the same patch:
```bash
gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
-f ref=<sha> \
-f include_repo_e2e=false \
-f include_release_path_suites=false \
-f include_openwebui=false \
-f docker_lanes='install-e2e bundled-channel-update-acpx' \
-f include_live_suites=false \
-f live_models_only=false
```
That path still runs the prepare job, so it creates a new tarball for `<sha>`.
If the SHA-tagged GHCR bare/functional image already exists, CI skips rebuilding
that image and only uploads the fresh package artifact before the targeted lane
job. Do not rerun the full release path unless the failed lane list
or touched surface really requires it.
## Docker Expected Timings
Treat these as ballpark. Blacksmith queue time, GHCR pull speed, provider
latency, npm cache state, and Docker daemon health can dominate.
Current local timing artifact (`.artifacts/docker-tests/lane-timings.json`) has
these rough bands:
- Tiny lanes, seconds to under 1 minute:
`agents-delete-shared-workspace` ~3s, `plugin-update` ~7s,
`config-reload` ~14s, `pi-bundle-mcp-tools` ~15s, `onboard` ~18s,
`session-runtime-context` ~20s, `gateway-network` ~34s, `qr` ~44s.
- Medium deterministic lanes, ~1-5 minutes:
`npm-onboard-channel-agent` ~96s, `openai-image-auth` ~99s,
bundled channel/update lanes usually ~90-300s when split, `openwebui` ~225s,
`mcp-channels` ~274s.
- Heavy deterministic lanes, ~6-10 minutes:
`bundled-channel-root-owned` ~429s,
`bundled-channel-setup-entry` ~420s,
`bundled-channel-load-failure` ~383s,
`cron-mcp-cleanup` ~567s.
- Live provider lanes, often ~15-20 minutes:
`live-gateway` ~958s, `live-models` ~1054s.
- Installer/release lanes:
`install-e2e` and package-update paths can vary widely with npm, provider,
and package registry behavior. Budget tens of minutes; prefer GitHub targeted
reruns over local repeats.
Default fallback lane timeout is 120 minutes. A timeout usually means debug the
lane log/artifacts first, not “run the whole thing again.”
## Failure Workflow
1. Identify exact failing job, SHA, lane, and artifact path.
2. Read `failures.json`, `summary.json`, and the failed lane log tail.
3. Use `pnpm test:docker:rerun <run-id|failures.json>` to generate targeted
GitHub rerun commands.
4. If the lane has `rerunCommand`, use that only as a local starting point.
5. For Docker release failures, dispatch targeted `docker_lanes=<failed-lane>`
on GitHub before considering local Docker.
6. Patch narrowly, then rerun the failed file/lane only.
7. Broaden to `pnpm check:changed` or CI only after the isolated proof passes.
## When To Escalate
- Public SDK/plugin contract changes: run changed gate plus relevant extension
validation.
- Build output, lazy imports, package boundaries, or published surfaces:
include `pnpm build`.
- Workflow edits: run `pnpm check:workflows`.
- Release branch or tag validation: use release docs and GitHub workflows; avoid
local Docker unless Peter explicitly asks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "OpenClaw Testing"
short_description: "Choose cheap, targeted OpenClaw validation"
default_prompt: "Use $openclaw-testing to choose the cheapest safe test or CI verification path, inspect failures, and rerun only the relevant OpenClaw lane."

View File

@@ -1,6 +1,6 @@
---
name: tag-duplicate-prs-issues
description: Search duplicate OpenClaw PRs/issues, group related work in prtags, and sync duplicate state to GitHub.
description: Use gitcrawl to search duplicate OpenClaw PRs/issues, group related work in prtags, and sync duplicate state to GitHub.
---
# Tag Duplicate PRs and Issues
@@ -12,43 +12,25 @@ It is not for reviewing the implementation quality of a PR.
## Required Setup
Do not start duplicate triage until this setup is complete.
Do not write duplicate groups or annotations until this setup is complete.
Read-only discovery can still proceed with `gitcrawl` and live `gh`.
### Install the companion skills
### Companion Skills
Install these skills first because they teach the agent how to use the two main CLIs correctly:
- `ghreplica` skill from the `ghreplica` repo at `skills/ghreplica/SKILL.md`
- `prtags` skill from the `prtags` repo at `skills/prtags/SKILL.md`
This skill assumes those two skills are available and can be used during the same run.
Use `$gitcrawl` first for local candidate discovery.
Use the `prtags` skill from the `prtags` repo at `skills/prtags/SKILL.md` when it is available.
### Install the CLIs
Install `ghreplica` and `prtags` from their latest GitHub releases.
Install `prtags` from its latest GitHub release.
Do not rely on an old local build unless the maintainer explicitly wants to test unreleased behavior.
`ghreplica` CLI install path:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/ghreplica/main/scripts/install-ghr.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
`prtags` CLI install path:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/prtags/main/scripts/install-prtags.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
Use the `pr-search-cli` project with `uvx`.
The command itself is `pr-search`.
Do not require a permanent install unless the maintainer explicitly wants one.
```bash
uvx --from pr-search-cli pr-search status
uvx --from pr-search-cli pr-search code similar 67144
```
### Authenticate prtags
`prtags` should be logged in with the maintainer's own GitHub account through OAuth device flow.
@@ -66,20 +48,15 @@ The expected outcome is that `prtags` stores the logged-in maintainer identity l
Do not require an up-front preflight before starting the workflow.
Proceed with the normal steps until you actually need a tool or account state.
As soon as you discover that a required CLI is missing or `prtags` is not logged in, stop immediately.
Do not continue in a partial mode after that point.
As soon as you discover that `prtags` is missing or not logged in at the write step, stop immediately.
Do not continue in a partial write mode after that point.
If `ghr` is missing, ask the user to run the `ghreplica` install command.
If `prtags` is missing, ask the user to run both CLI install commands:
If `prtags` is missing, ask the user to run:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/ghreplica/main/scripts/install-ghr.sh | bash -s -- --bin-dir "$HOME/.local/bin"
curl -fsSL https://raw.githubusercontent.com/dutifuldev/prtags/main/scripts/install-prtags.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
If `uvx --from pr-search-cli pr-search ...` fails because `uvx` or the `pr-search` launcher is not available, ask the user to make that command work before continuing.
If `prtags auth status` shows that the user is not logged in, ask the user to run:
```bash
@@ -90,19 +67,19 @@ Resume only after the missing tool or login state has been fixed.
## Read-Path Default
For read-only GitHub operations in this workflow, use `ghr` as the default CLI.
Treat it as a drop-in replacement for the `gh` read operations you would normally use for PRs, issues, comments, reviews, and duplicate-search evidence.
For candidate discovery in this workflow, use `gitcrawl` first.
Treat it as the local history and clustering layer for related issues, duplicate attempts, and closed threads.
Only fall back to `gh` when `ghr` is failing for a concrete reason, such as:
Use live `gh` or `gh api` for the target thread and for any candidate before making an actionable judgment.
Use live GitHub when `gitcrawl` is missing or stale for a concrete reason, such as:
- the mirrored object is not present yet
- the mirror data is clearly stale or incomplete for the decision you need to make
- the `ghr` command errors, times out, or does not expose the specific read you need
- the target or candidate is not present yet
- the local data is clearly stale or incomplete for the decision you need to make
- `gitcrawl` errors, times out, or lacks the needed neighbor/search data
When you fall back to `gh`, note that you did so and why.
When you fall back to live GitHub search, note that you did so and why.
If `ghr` is missing a fresh PR or issue but `gh` can read it, you may use `gh` for the read-side judgment.
If a later `prtags` target-level write fails because the same object is still missing from `ghreplica`, stop and report that the mirror has not caught up yet instead of forcing the write.
If a later `prtags` target-level write fails because its own mirror has not caught up, stop and report that the curation backend is missing the target object instead of forcing a fallback write.
## Goal
@@ -118,14 +95,12 @@ For each target PR or issue:
Use the tools with these boundaries:
- `ghreplica` is the raw evidence source
- use `ghr` first for normal GitHub read operations in this workflow
- use it for title/body/comment search, related PRs, overlapping files, overlapping ranges, and current PR or issue status
- resort to `gh` only when `ghr` cannot provide the needed read cleanly
- `pr-search-cli` is candidate generation and ranking
- use it to suggest likely duplicate PRs or issue-cluster context
- do not treat it as final truth
- do not create or expand a duplicate group only because `pr-search-cli` put multiple PRs in the same issue or duplicate cluster
- `gitcrawl` is candidate generation and historical context
- use it first for local title/body search, neighbors, clusters, and closed-thread discovery
- treat every candidate as a lead until live GitHub confirms it
- `gh` is live GitHub truth
- use it for target state, body, comments, reviews, files, linked issues, and current open/closed/merged status
- use `gh search` only when `gitcrawl` is stale, missing data, or cannot express the needed query
- `prtags` is the maintainer curation layer
- use it to create or reuse one duplicate group
- use it to save the duplicate status, confidence, rationale, and group summary
@@ -182,7 +157,7 @@ Examples:
## Evidence Checklist
Before declaring a duplicate, gather evidence from at least two categories.
Same-issue or same-cluster output from `pr-search-cli` counts only as candidate generation, not as one of the required proof categories by itself.
`gitcrawl` neighbors, search hits, and cluster membership count as candidate generation, not as enough proof by themselves.
For PRs:
@@ -205,21 +180,18 @@ If you only have wording similarity, that is not enough.
## Step 1: Read The Target
Start by reading the target itself.
Use `ghr` first for this step even if you would normally reach for `gh`.
Use live GitHub for current target state.
For a PR:
```bash
ghr pr view -R openclaw/openclaw <number> --comments
ghr pr reviews -R openclaw/openclaw <number>
ghr pr comments -R openclaw/openclaw <number>
gh pr view <number> --json number,title,state,mergedAt,body,closingIssuesReferences,files,comments,reviews,statusCheckRollup
```
For an issue:
```bash
ghr issue view -R openclaw/openclaw <number> --comments
ghr issue comments -R openclaw/openclaw <number>
gh issue view <number> --json number,title,state,body,comments,closedAt
```
Record:
@@ -232,74 +204,56 @@ Record:
- whether it is open, closed, or merged
- whether there is already a likely duplicate thread mentioned by humans
## Step 2: Search Broadly With ghreplica
## Step 2: Search Broadly With Gitcrawl
Use `ghreplica` first because it is the most direct evidence source.
Do not switch to `gh` for ordinary reads unless `ghr` is missing data or failing.
Use `gitcrawl` first because it is the local OpenClaw history and clustering source.
Do not switch to broad live GitHub search unless `gitcrawl` is missing data, stale, or failing.
### PR duplicate search
Run all of these when the target is a PR:
Start with the target and nearby threads:
```bash
ghr search related-prs -R openclaw/openclaw <pr-number> --mode path_overlap --state all
ghr search related-prs -R openclaw/openclaw <pr-number> --mode range_overlap --state all
ghr search mentions -R openclaw/openclaw --query "<key phrase from title or body>" --mode fts --scope pull_requests --state all
ghr search mentions -R openclaw/openclaw --query "<subsystem or error phrase>" --mode fts --scope issues --state all
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 20 --json
```
Use `prs-by-paths` or `prs-by-ranges` when the likely duplicate surface is already known:
Then search key phrases and subsystem terms:
```bash
ghr search prs-by-paths -R openclaw/openclaw --path src/example.ts --state all
ghr search prs-by-ranges -R openclaw/openclaw --path src/example.ts --start 20 --end 80 --state all
gitcrawl search openclaw/openclaw --query "<key phrase from title or body>" --mode hybrid --limit 20 --json
gitcrawl search openclaw/openclaw --query "<subsystem or error phrase>" --mode hybrid --limit 20 --json
```
### Issue duplicate search
`ghreplica` does not have a special issue-to-issue “related issues” command.
For issues, search mirrored text and linked PR context instead.
Run targeted text searches:
Inspect likely clusters:
```bash
ghr search mentions -R openclaw/openclaw --query "<issue title phrase>" --mode fts --scope issues --state all
ghr search mentions -R openclaw/openclaw --query "<error message or symptom>" --mode fts --scope issues --state all
ghr search mentions -R openclaw/openclaw --query "<subsystem phrase>" --mode fts --scope pull_requests --state all
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
Then inspect the candidate PRs or issues those searches uncover.
## Step 3: Use pr-search-cli As A Hint Layer
Use `pr-search-cli` after `ghreplica`.
It is good at surfacing candidates quickly, but it is not the final decision-maker.
Run it through the `pr-search` command.
For a PR:
For PRs, verify likely code overlap with live file data:
```bash
uvx --from pr-search-cli pr-search -R openclaw/openclaw code similar <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw code clusters for-pr <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw issues for-pr <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw issues duplicate-prs
gh pr view <candidate-pr> --json number,title,state,mergedAt,files,body,comments,reviews
```
Interpretation:
For issues, verify likely duplicate issue state and comments live:
- `code similar` suggests PRs with similar change shape
- `code clusters for-pr` shows the PRs nearby code cluster
- `issues for-pr` shows which issue clusters the PR appears to belong to
- `issues duplicate-prs` is useful for spotting already-known duplicate PR patterns
```bash
gh issue view <candidate-issue> --json number,title,state,body,comments,closedAt
```
Treat every `pr-search-cli` result as a hint to investigate, not as enough evidence to create or widen a duplicate group.
Multiple PRs can share the same issue or issue cluster while still taking meaningfully different fix paths.
## Step 3: Use Live GitHub Search For Gaps
For an issue:
Use targeted live GitHub search after `gitcrawl` when:
- use `ghreplica` first to find candidate PRs or issue wording
- if the issue has linked PRs or a likely implementation PR, run `pr-search-cli` on those PRs
- treat issue-cluster output as supporting context, not as enough by itself to call the issue a duplicate
- the target is too new for the local store
- comments or reviews matter and the local store lacks them
- the exact phrase did not appear in local results but the issue/PR is current enough that GitHub should know it
```bash
gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "<key phrase>"
gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "<key phrase>"
gh search issues --repo openclaw/openclaw --match comments --limit 50 -- "<error or maintainer phrase>"
```
## Step 4: Decide The Outcome
@@ -344,7 +298,7 @@ Reuse an existing group when:
- it already contains clearly related members
- adding the target would keep the group coherent
Do not widen an existing group just because `pr-search-cli` placed several PRs under the same issue or duplicate cluster.
Do not widen an existing group just because `gitcrawl` placed several PRs or issues near each other.
Confirm that the actual implementation path and maintainer intent still match before adding the new member.
Create a new group only when no existing group clearly fits.
@@ -423,8 +377,8 @@ prtags annotation group set <group-id> \
When the evidence is incomplete, set `duplicate_status=candidate` and lower the confidence.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target through `ghreplica`, do not force a fallback write path.
Keep the group state you were able to write, report that the mirror is still missing the target object, and defer the target-level annotation until `ghreplica` catches up.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target, do not force a fallback write path.
Keep the group state you were able to write, report that the curation backend is still missing the target object, and defer the target-level annotation until `prtags` catches up.
## Step 8: Let prtags Sync The Group Comment

View File

@@ -1,4 +1,4 @@
interface:
display_name: "Tag Duplicate PRs and Issues"
short_description: "Find duplicate PRs and issues, group them in prtags, and let prtags sync the GitHub comment"
default_prompt: "Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather evidence with ghreplica and pr-search-cli, group related items in prtags, and save the duplicate judgment."
short_description: "Find duplicate PRs and issues with gitcrawl, group them in prtags, and let prtags sync the GitHub comment"
default_prompt: "Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather candidates with gitcrawl, verify live state with GitHub, group related items in prtags, and save the duplicate judgment."

1
.github/CODEOWNERS vendored
View File

@@ -9,6 +9,7 @@
/.github/dependabot.yml @openclaw/secops
/.github/codeql/ @openclaw/secops
/.github/workflows/codeql.yml @openclaw/secops
/.github/workflows/codeql-critical-quality.yml @openclaw/secops
/src/security/ @openclaw/secops
/src/secrets/ @openclaw/secops
/src/config/*secret*.ts @openclaw/secops

View File

@@ -0,0 +1,149 @@
name: Docker E2E plan and hydrate
description: >
Create a Docker E2E lane plan, expose GitHub outputs, and optionally hydrate
the prebuilt package artifact plus shared Docker images needed by the plan.
inputs:
mode:
description: prepare, chunk, or targeted.
required: true
chunk:
description: Release-path chunk for mode=chunk.
required: false
default: ""
lanes:
description: Comma/space separated lane names for targeted or prepare mode.
required: false
default: ""
include-openwebui:
description: Whether Open WebUI is included when planning release/prepare coverage.
required: false
default: "true"
include-release-path-suites:
description: Whether prepare mode should plan all release-path suites.
required: false
default: "false"
hydrate-artifacts:
description: Whether to download/pull artifacts required by the plan.
required: false
default: "true"
package-artifact-name:
description: Workflow artifact name containing openclaw-current.tgz.
required: false
default: docker-e2e-package
outputs:
credentials:
description: Comma-separated credential groups required by selected lanes.
value: ${{ steps.plan.outputs.credentials }}
needs_bare_image:
description: "1 when selected lanes require the bare Docker E2E image."
value: ${{ steps.plan.outputs.needs_bare_image }}
needs_e2e_image:
description: "1 when selected lanes require any Docker E2E image."
value: ${{ steps.plan.outputs.needs_e2e_image }}
needs_functional_image:
description: "1 when selected lanes require the functional Docker E2E image."
value: ${{ steps.plan.outputs.needs_functional_image }}
needs_live_image:
description: "1 when selected lanes require building the live Docker image."
value: ${{ steps.plan.outputs.needs_live_image }}
needs_package:
description: "1 when selected lanes require the OpenClaw package tarball."
value: ${{ steps.plan.outputs.needs_package }}
plan_json:
description: Path to the generated plan JSON.
value: ${{ steps.plan.outputs.plan_json }}
runs:
using: composite
steps:
- name: Plan Docker E2E lanes
id: plan
shell: bash
env:
MODE: ${{ inputs.mode }}
CHUNK: ${{ inputs.chunk }}
LANES: ${{ inputs.lanes }}
INCLUDE_OPENWEBUI: ${{ inputs.include-openwebui }}
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include-release-path-suites }}
run: |
set -euo pipefail
mkdir -p .artifacts/docker-tests
case "$MODE" in
prepare)
plan_path=".artifacts/docker-tests/plan.json"
if [[ "$INCLUDE_RELEASE_PATH_SUITES" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_PLAN_RELEASE_ALL=1
elif [[ -n "$LANES" ]]; then
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
elif [[ "$INCLUDE_OPENWEBUI" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_LANES=openwebui
fi
;;
chunk)
if [[ -z "$CHUNK" ]]; then
echo "chunk input is required for Docker E2E chunk planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_CHUNK="$CHUNK"
plan_path=".artifacts/docker-tests/release-${CHUNK}-plan.json"
;;
targeted)
if [[ -z "$LANES" ]]; then
echo "lanes input is required for Docker E2E targeted planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
plan_path=".artifacts/docker-tests/targeted-plan.json"
;;
*)
echo "mode must be prepare, chunk, or targeted. Got: $MODE" >&2
exit 1
;;
esac
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="$INCLUDE_OPENWEBUI"
node scripts/test-docker-all.mjs --plan-json > "$plan_path"
node scripts/docker-e2e.mjs github-outputs "$plan_path" >> "$GITHUB_OUTPUT"
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"
- name: Download OpenClaw Docker E2E package
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
with:
name: ${{ inputs.package-artifact-name }}
path: .artifacts/docker-e2e-package
- name: Pull shared bare Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_bare_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
- name: Pull shared functional Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_functional_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
- name: Validate Docker E2E credentials
if: inputs.hydrate-artifacts == 'true'
shell: bash
env:
CREDENTIALS: ${{ steps.plan.outputs.credentials }}
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi

View File

@@ -0,0 +1,8 @@
name: openclaw-codeql-actions-critical-security
paths:
- .github/actions
- .github/workflows
paths-ignore:
- .github/workflows/stale.yml

View File

@@ -0,0 +1,30 @@
name: openclaw-codeql-android-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
# Android canvas intentionally runs trusted A2UI JavaScript; keep this profile focused on exploitable WebView edges.
- exclude:
id: java/android/websettings-javascript-enabled
# Gateway TLS already pins verified certificate SHA-256 fingerprints. OkHttp CertificatePinner pins SPKI hashes,
# so this query is noisy for OpenClaw's TOFU/local-gateway trust model and does not belong in the critical profile.
- exclude:
id: java/android/missing-certificate-pinning
paths:
- apps/android/app/src/main
paths-ignore:
- "**/.gradle"
- "**/build"
- "**/node_modules"
- "**/coverage"
- "**/*.generated.*"
- "**/*Test.kt"
- "**/*Test.java"
- "**/*Benchmark.kt"
- apps/android/app/src/test
- apps/android/benchmark

View File

@@ -0,0 +1,54 @@
name: openclaw-codeql-javascript-typescript-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/agents/*auth*.ts
- src/agents/**/*auth*.ts
- src/agents/auth-health*.ts
- src/agents/auth-profiles
- src/agents/bash-tools.exec-host-shared.ts
- src/agents/sandbox
- src/agents/sandbox.ts
- src/agents/sandbox-*.ts
- src/config
- src/cron/service/jobs.ts
- src/cron/stagger.ts
- src/gateway/*auth*.ts
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts
- src/infra/secret-file*.ts
- src/secrets
- src/security
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -0,0 +1,57 @@
name: openclaw-codeql-javascript-typescript-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
- exclude:
problem.severity:
- recommendation
- warning
paths:
- src/agents/*auth*.ts
- src/agents/**/*auth*.ts
- src/agents/auth-health*.ts
- src/agents/auth-profiles
- src/agents/bash-tools.exec-host-shared.ts
- src/agents/sandbox
- src/agents/sandbox.ts
- src/agents/sandbox-*.ts
- src/config/*secret*.ts
- src/config/**/*secret*.ts
- src/cron/service/jobs.ts
- src/cron/stagger.ts
- src/gateway/*auth*.ts
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts
- src/infra/secret-file*.ts
- src/secrets
- src/security
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,21 +0,0 @@
name: openclaw-codeql-javascript-typescript
paths:
- src
- extensions
- ui/src
- skills
paths-ignore:
- apps
- dist
- docs
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"

View File

@@ -0,0 +1,17 @@
name: openclaw-codeql-macos-critical-security
disable-default-queries: true
queries:
- uses: security-extended
paths:
- apps/macos/Sources
paths-ignore:
- "**/.build"
- "**/.build/**"
- "**/DerivedData"
- "**/DerivedData/**"
- "**/*.generated.swift"
- "**/*Tests.swift"

30
.github/labeler.yml vendored
View File

@@ -35,6 +35,17 @@
- any-glob-to-any-file:
- "extensions/google-meet/**"
- "docs/plugins/google-meet.md"
"plugin: migrate-hermes":
- changed-files:
- any-glob-to-any-file:
- "extensions/migrate-hermes/**"
- "docs/cli/migrate.md"
"plugin: migrate-claude":
- changed-files:
- any-glob-to-any-file:
- "extensions/migrate-claude/**"
- "docs/cli/migrate.md"
- "docs/install/migrating-claude.md"
"plugin: bonjour":
- changed-files:
- any-glob-to-any-file:
@@ -101,6 +112,11 @@
- any-glob-to-any-file:
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: synology-chat":
- changed-files:
- any-glob-to-any-file:
- "extensions/synology-chat/**"
- "docs/channels/synology-chat.md"
"channel: telegram":
- changed-files:
- any-glob-to-any-file:
@@ -233,6 +249,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-otel/**"
"extensions: diagnostics-prometheus":
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-prometheus/**"
"extensions: llm-task":
- changed-files:
- any-glob-to-any-file:
@@ -285,10 +305,20 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/byteplus/**"
"extensions: cerebras":
- changed-files:
- any-glob-to-any-file:
- "extensions/cerebras/**"
- "docs/providers/cerebras.md"
"extensions: deepseek":
- changed-files:
- any-glob-to-any-file:
- "extensions/deepseek/**"
"extensions: deepinfra":
- changed-files:
- any-glob-to-any-file:
- "extensions/deepinfra/**"
- "docs/providers/deepinfra.md"
"extensions: tencent":
- changed-files:
- any-glob-to-any-file:

View File

@@ -0,0 +1,224 @@
name: Blacksmith Build Artifacts Testbox
on:
workflow_dispatch:
inputs:
testbox_id:
type: string
description: "Testbox session ID"
required: true
pull_request:
paths:
- ".github/workflows/**"
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
build-artifacts:
permissions:
contents: read
name: "build-artifacts"
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 35
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@v2
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Resolve release dist cache seeds
id: dist-cache-seeds
shell: bash
run: |
set -euo pipefail
cache_prefix="${RUNNER_OS}-dist-build-"
declare -A seen=()
resolve_tag_sha() {
local tag="$1"
local direct=""
local peeled=""
while read -r sha ref; do
if [[ "$ref" == "refs/tags/${tag}^{}" ]]; then
peeled="$sha"
elif [[ "$ref" == "refs/tags/${tag}" ]]; then
direct="$sha"
fi
done < <(git ls-remote --tags origin "refs/tags/${tag}" "refs/tags/${tag}^{}")
printf '%s\n' "${peeled:-$direct}"
}
{
echo "restore-keys<<EOF"
for dist_tag in beta latest; do
version="$(npm view "openclaw@${dist_tag}" version 2>/dev/null || true)"
if [[ -z "$version" ]]; then
echo "Could not resolve npm dist-tag ${dist_tag}; skipping cache seed." >&2
continue
fi
sha="$(resolve_tag_sha "v${version}")"
if [[ -z "$sha" ]]; then
echo "Could not resolve git tag v${version}; skipping cache seed." >&2
continue
fi
key="${cache_prefix}${sha}"
if [[ -z "${seen[$key]+x}" ]]; then
echo "$key"
seen[$key]=1
fi
done
echo "${cache_prefix}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Restore dist build cache
id: dist-cache
uses: actions/cache/restore@v5
with:
path: |
.artifacts/build-all-cache/
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
restore-keys: ${{ steps.dist-cache-seeds.outputs.restore-keys }}
- name: Build dist on cache miss
if: steps.dist-cache.outputs.cache-hit != 'true'
run: pnpm build:ci-artifacts
- name: Build Control UI on cache miss
if: steps.dist-cache.outputs.cache-hit != 'true'
run: pnpm ui:build
- name: Verify build artifacts
shell: bash
run: |
set -euo pipefail
test -d dist
test -d dist-runtime
if [[ ! -f dist/index.js && ! -f dist/index.mjs ]]; then
echo "Missing dist/index.js or dist/index.mjs" >&2
exit 1
fi
test -f dist/build-info.json
test -f dist/control-ui/index.html
- name: Save dist build cache
if: steps.dist-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: |
.artifacts/build-all-cache/
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@v2
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -93,6 +93,33 @@ jobs:
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@v2
if: always()

View File

@@ -1,6 +1,13 @@
name: CI
on:
workflow_dispatch:
inputs:
target_ref:
description: Optional branch, tag, or full commit SHA to validate instead of the workflow ref
required: false
default: ""
type: string
push:
branches: [main]
paths-ignore:
@@ -13,8 +20,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha)) }}
cancel-in-progress: true
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -29,6 +36,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
checkout_sha: ${{ steps.checkout_ref.outputs.sha }}
docs_only: ${{ steps.manifest.outputs.docs_only }}
docs_changed: ${{ steps.manifest.outputs.docs_changed }}
run_node: ${{ steps.manifest.outputs.run_node }}
@@ -37,8 +45,6 @@ jobs:
run_skills_python: ${{ steps.manifest.outputs.run_skills_python }}
run_skills_python_job: ${{ steps.manifest.outputs.run_skills_python_job }}
run_windows: ${{ steps.manifest.outputs.run_windows }}
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
@@ -51,8 +57,6 @@ jobs:
checks_node_core_nondist_matrix: ${{ steps.manifest.outputs.checks_node_core_nondist_matrix }}
run_checks_node_core_dist: ${{ steps.manifest.outputs.run_checks_node_core_dist }}
checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }}
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
run_check: ${{ steps.manifest.outputs.run_check }}
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
@@ -69,12 +73,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Resolve checkout SHA
id: checkout_ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@@ -82,11 +92,12 @@ jobs:
- name: Detect docs-only changes
id: docs_scope
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/detect-docs-changes
- name: Detect changed scopes
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
@@ -99,45 +110,20 @@ jobs:
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Detect changed extensions
id: changed_extensions
if: steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs";
const extensionIds = listChangedExtensionIds({
base: process.env.BASE_SHA,
head: "HEAD",
fallbackBaseRef: process.env.BASE_REF,
unavailableBaseBehavior: "all",
});
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
EOF
- name: Build CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_DOCS_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
run: |
node --input-type=module <<'EOF'
@@ -161,18 +147,8 @@ jobs:
return fallback;
};
const parseJson = (value, fallback) => {
try {
return value ? JSON.parse(value) : fallback;
} catch {
return fallback;
}
};
const createMatrix = (include) => ({ include });
const outputPath = process.env.GITHUB_OUTPUT;
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
const isPush = eventName === "push";
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
@@ -197,11 +173,6 @@ jobs:
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
const hasChangedExtensions =
parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly;
const changedExtensionsMatrix = hasChangedExtensions
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
: { include: [] };
const extensionTestShardCount = isCanonicalRepository
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
@@ -271,8 +242,6 @@ jobs:
run_android: runAndroid,
run_skills_python: runSkillsPython,
run_windows: runWindows,
has_changed_extensions: hasChangedExtensions,
changed_extensions_matrix: changedExtensionsMatrix,
run_build_artifacts: runNodeFull,
run_checks_fast_core: runChecksFastCore,
run_checks_fast: runNodeFull,
@@ -293,15 +262,6 @@ jobs:
checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
run_checks_node_core_dist: nodeTestDistShards.length > 0,
checks_node_core_dist_matrix: createMatrix(nodeTestDistShards),
run_extension_fast: hasChangedExtensions && !isPush,
extension_fast_matrix: createMatrix(
hasChangedExtensions && !isPush
? (changedExtensionsMatrix.include ?? []).map((entry) => ({
check_name: `extension-fast-${entry.extension}`,
extension: entry.extension,
}))
: [],
),
run_check: runNodeFull,
run_check_additional: runNodeFull,
run_build_smoke: runNodeFull,
@@ -354,12 +314,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Ensure security base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@@ -443,6 +405,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
@@ -505,7 +468,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -577,7 +540,7 @@ jobs:
path: |
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_sha }}
- name: Pack built runtime artifacts
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
@@ -706,7 +669,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -801,7 +764,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -904,7 +867,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -972,7 +935,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1084,7 +1047,7 @@ jobs:
contents: read
name: checks-node-compat-node22
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'push'
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
steps:
@@ -1092,7 +1055,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1172,7 +1135,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1323,84 +1286,6 @@ jobs:
exit 1
fi
extension-fast:
permissions:
contents: read
name: "extension-fast"
needs: [preflight]
if: needs.preflight.outputs.run_extension_fast == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run changed extension tests
env:
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
run: |
set -euo pipefail
if [ "$OPENCLAW_CHANGED_EXTENSION" = "telegram" ]; then
export OPENCLAW_VITEST_MAX_WORKERS=1
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=6144"
pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION" -- --pool=forks
exit 0
fi
pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
# Types, lint, and format check shards.
check-shard:
permissions:
@@ -1437,7 +1322,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1569,7 +1454,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1767,7 +1652,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1830,6 +1715,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_sha }}
persist-credentials: false
submodules: false
@@ -1872,6 +1758,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_sha }}
persist-credentials: false
submodules: false
@@ -1976,6 +1863,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_sha }}
persist-credentials: false
submodules: false
@@ -2016,6 +1904,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_sha }}
persist-credentials: false
submodules: false
@@ -2116,7 +2005,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

View File

@@ -0,0 +1,50 @@
name: ClawSweeper Dispatch
on:
issues:
types: [opened, reopened, edited, labeled, unlabeled]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution
types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled]
permissions:
contents: read
jobs:
dispatch:
runs-on: ubuntu-latest
env:
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
steps:
- name: Create ClawSweeper dispatch token
id: token
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
uses: actions/create-github-app-token@v2
with:
app-id: 3306130
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: openclaw
repositories: clawsweeper
- name: Dispatch exact ClawSweeper review
env:
GH_TOKEN: ${{ steps.token.outputs.token || secrets.OPENCLAW_GH_TOKEN }}
TARGET_REPO: ${{ github.repository }}
ITEM_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
ITEM_KIND: ${{ github.event_name == 'pull_request_target' && 'pull_request' || 'issue' }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured."
exit 0
fi
payload="$(jq -nc \
--arg target_repo "$TARGET_REPO" \
--argjson item_number "$ITEM_NUMBER" \
--arg item_kind "$ITEM_KIND" \
'{event_type:"clawsweeper_item",client_payload:{target_repo:$target_repo,item_number:$item_number,item_kind:$item_kind}}')"
if gh api repos/openclaw/clawsweeper/dispatches \
--method POST \
--input - <<< "$payload"; then
echo "Dispatched ClawSweeper review."
else
echo "::warning::Skipping ClawSweeper dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
fi

View File

@@ -0,0 +1,40 @@
name: CodeQL Critical Quality
on:
workflow_dispatch:
schedule:
- cron: "30 6 * * *"
concurrency:
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
security-events: write
jobs:
javascript-typescript:
name: Critical Quality (javascript-typescript)
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-javascript-typescript-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/javascript-typescript"

View File

@@ -2,12 +2,23 @@ name: CodeQL
on:
workflow_dispatch:
inputs:
profile:
description: CodeQL security profile to run
required: false
default: all
type: choice
options:
- all
- security
- android-security
- macos-security
schedule:
- cron: "0 6 * * *"
concurrency:
group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -18,121 +29,140 @@ permissions:
security-events: write
jobs:
analyze:
name: Analyze (${{ matrix.language }})
critical-security:
name: Critical Security (${{ matrix.language }})
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'security' }}
runs-on: ${{ matrix.runs_on }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
runs_on: blacksmith-32vcpu-ubuntu-2404
needs_node: true
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ./.github/codeql/codeql-javascript-typescript.yml
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-javascript-typescript-critical-security.yml
- language: actions
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: python
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: true
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: java-kotlin
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: true
needs_swift_tools: false
needs_manual_build: true
needs_autobuild: false
config_file: ""
- language: swift
runs_on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: true
needs_manual_build: true
needs_autobuild: false
config_file: ""
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 10
config_file: ./.github/codeql/codeql-actions-critical-security.yml
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Setup Node environment
if: matrix.needs_node
uses: ./.github/actions/setup-node-env
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
install-bun: "false"
languages: ${{ matrix.language }}
config-file: ${{ matrix.config_file }}
- name: Setup Python
if: matrix.needs_python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
python-version: "3.12"
category: "/codeql-critical-security/${{ matrix.language }}"
android-security:
name: Critical Security (android)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.profile == 'android-security' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Setup Java
if: matrix.needs_java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- name: Setup Swift build tools
if: matrix.needs_swift_tools
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
brew install xcodegen swiftlint swiftformat
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
config-file: ${{ matrix.config_file || '' }}
- name: Autobuild
if: matrix.needs_autobuild
uses: github/codeql-action/autobuild@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
languages: java-kotlin
build-mode: manual
config-file: ./.github/codeql/codeql-android-critical-security.yml
- name: Build Android for CodeQL
if: matrix.language == 'java-kotlin'
working-directory: apps/android
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Build Swift for CodeQL
if: matrix.language == 'swift'
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-security/android"
macos-security:
name: Critical Security (macOS)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.profile == 'macos-security' }}
runs-on: blacksmith-6vcpu-macos-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Select Xcode
run: |
set -euo pipefail
swift build --package-path apps/macos --configuration release
cd apps/ios
xcodegen generate
xcodebuild build \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "generic/platform=iOS Simulator" \
CODE_SIGNING_ALLOWED=NO
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: swift
build-mode: manual
config-file: ./.github/codeql/codeql-macos-critical-security.yml
- name: Build macOS for CodeQL
run: swift build --package-path apps/macos --product OpenClaw
- name: Analyze
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
id: analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/language:${{ matrix.language }}"
output: sarif-results
upload: failure-only
category: "/codeql-critical-security/macos"
- name: Remove dependency build results
env:
SARIF_OUTPUT: sarif-results
run: |
set -euo pipefail
shopt -s nullglob
if [ ! -d "$SARIF_OUTPUT" ]; then
echo "SARIF output directory not found: $SARIF_OUTPUT" >&2
exit 1
fi
mkdir -p sarif-results-filtered
files=("$SARIF_OUTPUT"/*.sarif)
if [ "${#files[@]}" -eq 0 ]; then
echo "No SARIF files found in $SARIF_OUTPUT" >&2
exit 1
fi
for file in "${files[@]}"; do
jq '
def in_dependency_build:
((.locations // []) | length > 0)
and all(.locations[]; (.physicalLocation.artifactLocation.uri? // "") | test("^apps/macos/\\.build/"));
.runs |= map(.results = ((.results // []) | map(select(in_dependency_build | not))))
' "$file" > "sarif-results-filtered/$(basename "$file")"
done
- name: Upload filtered SARIF
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
sarif_file: sarif-results-filtered
category: "/codeql-critical-security/macos"

View File

@@ -55,6 +55,7 @@ jobs:
# WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
runs-on: ubuntu-24.04
environment: docker-release
permissions: {}
steps:
- name: Approve Docker backfill
env:
@@ -63,7 +64,7 @@ jobs:
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
# Build amd64 images (default + slim share the build stage cache)
# Build amd64 image. Default and slim tags point to the same slim runtime.
build-amd64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -74,7 +75,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -117,12 +117,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -159,28 +154,15 @@ jobs:
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
# Build arm64 images (default + slim share the build stage cache)
# Build arm64 image. Default and slim tags point to the same slim runtime.
build-arm64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -191,7 +173,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -234,12 +215,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -276,25 +252,12 @@ jobs:
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
# Create multi-platform manifests
@@ -351,16 +314,11 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create and push default manifest
- name: Create and push manifest
shell: bash
env:
TAGS: ${{ steps.tags.outputs.value }}
@@ -378,20 +336,94 @@ jobs:
"${AMD64_DIGEST}" \
"${ARM64_DIGEST}"
- name: Create and push slim manifest
verify-attestations:
needs: [create-manifest]
if: ${{ always() && needs.create-manifest.result == 'success' }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image refs
id: refs
shell: bash
env:
SLIM_TAGS: ${{ steps.tags.outputs.slim }}
AMD64_SLIM_DIGEST: ${{ needs.build-amd64.outputs.slim-digest }}
ARM64_SLIM_DIGEST: ${{ needs.build-arm64.outputs.slim-digest }}
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
run: |
set -euo pipefail
mapfile -t tags <<< "${SLIM_TAGS}"
args=()
for tag in "${tags[@]}"; do
[ -z "$tag" ] && continue
args+=("-t" "$tag")
done
docker buildx imagetools create "${args[@]}" \
"${AMD64_SLIM_DIGEST}" \
"${ARM64_SLIM_DIGEST}"
multi_refs=()
slim_multi_refs=()
amd64_refs=()
arm64_refs=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
multi_refs+=("${IMAGE}:main")
slim_multi_refs+=("${IMAGE}:main-slim")
amd64_refs+=("${IMAGE}:main-amd64" "${IMAGE}:main-slim-amd64")
arm64_refs+=("${IMAGE}:main-arm64" "${IMAGE}:main-slim-arm64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
multi_refs+=("${IMAGE}:${version}")
slim_multi_refs+=("${IMAGE}:${version}-slim")
amd64_refs+=("${IMAGE}:${version}-amd64" "${IMAGE}:${version}-slim-amd64")
arm64_refs+=("${IMAGE}:${version}-arm64" "${IMAGE}:${version}-slim-arm64")
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
multi_refs+=("${IMAGE}:latest")
slim_multi_refs+=("${IMAGE}:slim")
fi
fi
if [[ ${#multi_refs[@]} -eq 0 || ${#amd64_refs[@]} -eq 0 || ${#arm64_refs[@]} -eq 0 ]]; then
echo "::error::No Docker image refs resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "multi<<EOF"
printf "%s\n" "${multi_refs[@]}" "${slim_multi_refs[@]}"
echo "EOF"
echo "amd64<<EOF"
printf "%s\n" "${amd64_refs[@]}"
echo "EOF"
echo "arm64<<EOF"
printf "%s\n" "${arm64_refs[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Verify Docker attestations
shell: bash
env:
MULTI_REFS: ${{ steps.refs.outputs.multi }}
AMD64_REFS: ${{ steps.refs.outputs.amd64 }}
ARM64_REFS: ${{ steps.refs.outputs.arm64 }}
run: |
set -euo pipefail
mapfile -t multi_refs <<< "${MULTI_REFS}"
mapfile -t amd64_refs <<< "${AMD64_REFS}"
mapfile -t arm64_refs <<< "${ARM64_REFS}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
--platform linux/arm64 \
"${multi_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
"${amd64_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/arm64 \
"${arm64_refs[@]}"

View File

@@ -197,7 +197,8 @@ jobs:
- name: Restore Node 24 path
if: steps.gate.outputs.run_agent == 'true'
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
run:
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
set -euo pipefail
export PATH="${NODE_BIN}:${PATH}"
echo "${NODE_BIN}" >> "$GITHUB_PATH"

View File

@@ -0,0 +1,564 @@
name: Full Release Validation
on:
workflow_dispatch:
inputs:
ref:
description: Branch, tag, or full commit SHA to validate
required: true
default: main
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which cross-OS release lanes to run
required: false
default: both
type: choice
options:
- fresh
- upgrade
- both
release_profile:
description: Release coverage profile for live/Docker/provider breadth
required: false
default: full
type: choice
options:
- minimum
- stable
- full
rerun_group:
description: Validation group to run
required: false
default: all
type: choice
options:
- all
- ci
- release-checks
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
- npm-telegram
npm_telegram_package_spec:
description: Optional published package spec for the post-publish Telegram E2E lane
required: false
default: ""
type: string
evidence_package_spec:
description: Optional published package spec to prove in the private release evidence report
required: false
default: ""
type: string
npm_telegram_provider_mode:
description: Provider mode for the optional post-publish Telegram E2E lane
required: false
default: mock-openai
type: choice
options:
- mock-openai
- live-frontier
npm_telegram_scenario:
description: Optional comma-separated Telegram scenario ids for the post-publish lane
required: false
default: ""
type: string
permissions:
actions: write
contents: read
concurrency:
group: full-release-validation-${{ inputs.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
GH_REPO: ${{ github.repository }}
jobs:
resolve_target:
name: Resolve target ref
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Checkout trusted workflow helper
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
path: workflow
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Resolve target SHA
id: resolve
env:
TARGET_REF: ${{ inputs.ref }}
run: |
bash workflow/scripts/github/resolve-openclaw-ref.sh \
--ref "$TARGET_REF" \
--github-output "$GITHUB_OUTPUT"
- name: Summarize target
env:
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
{
echo "## Full release validation"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Normal CI: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" != "ci" && "$RERUN_GROUP" != "npm-telegram" ]]; then
echo "- Release/live/Docker/package/QA: \`OpenClaw Release Checks\`"
else
echo "- Release/live/Docker/package/QA: skipped by rerun group"
fi
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
echo "- Post-publish Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
else
echo "- Post-publish Telegram E2E: skipped because no published package spec was provided"
fi
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
normal_ci:
name: Run normal full CI
needs: [resolve_target]
if: contains(fromJSON('["all","ci"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 240
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor CI
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
}
{
echo "### Normal CI"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA"
release_checks:
name: Run release/live/Docker/QA validation
needs: [resolve_target]
if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 720
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor release checks
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
}
{
echo "### Release/live/Docker/QA validation"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Provider: \`${PROVIDER}\`"
echo "- Cross-OS mode: \`${MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
} >> "$GITHUB_STEP_SUMMARY"
child_rerun_group="$RERUN_GROUP"
if [[ "$child_rerun_group" == "release-checks" ]]; then
child_rerun_group=all
fi
dispatch_and_wait openclaw-release-checks.yml \
-f ref="$TARGET_REF" \
-f expected_sha="$TARGET_SHA" \
-f provider="$PROVIDER" \
-f mode="$MODE" \
-f release_profile="$RELEASE_PROFILE" \
-f rerun_group="$child_rerun_group"
npm_telegram:
name: Run post-publish Telegram E2E
needs: [resolve_target]
if: inputs.npm_telegram_package_spec != '' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor npm Telegram E2E
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
SCENARIO: ${{ inputs.npm_telegram_scenario }}
run: |
set -euo pipefail
before_json="$(gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
if [[ -n "${SCENARIO// }" ]]; then
args+=(-f scenario="$SCENARIO")
fi
gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "Could not find dispatched run for npm-telegram-beta-e2e.yml." >&2
exit 1
fi
echo "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "npm-telegram-beta-e2e.yml finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
summary:
name: Verify full validation
needs: [normal_ci, release_checks, npm_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Request private evidence update
env:
RELEASE_PRIVATE_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN }}
TARGET_REF: ${{ inputs.ref }}
PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }}
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
run: |
set -euo pipefail
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" ]]; then
echo "Release checks were skipped by rerun group; skipping automatic private evidence update."
exit 0
fi
if [[ -z "${RELEASE_PRIVATE_DISPATCH_TOKEN// }" ]]; then
echo "OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN is not configured; skipping automatic private evidence update."
exit 0
fi
release_id="${TARGET_REF#refs/tags/}"
release_id="${release_id#v}"
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
release_id="${BASH_REMATCH[1]}"
fi
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
if [[ -z "$release_id" ]]; then
echo "::error::Could not derive release evidence id from target ref '${TARGET_REF}'."
exit 1
fi
payload="$(
jq -cn \
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
--arg release_id "$release_id" \
--arg release_ref "$TARGET_REF" \
--arg package_spec "$PACKAGE_SPEC" \
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
'{
event_type: "openclaw_full_release_validation_completed",
client_payload: {
full_validation_run_id: $full_validation_run_id,
release_id: $release_id,
release_ref: $release_ref,
package_spec: $package_spec,
notes: $notes
}
}'
)"
curl --fail-with-body \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${RELEASE_PRIVATE_DISPATCH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/openclaw/releases-private/dispatches \
-d "$payload"
- name: Verify child workflow results
env:
GH_TOKEN: ${{ github.token }}
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
run: |
set -euo pipefail
check_child() {
local label="$1"
local run_id="$2"
local required="$3"
if [[ -z "${run_id// }" ]]; then
if [[ "$required" == "0" ]]; then
echo "${label}: skipped"
return 0
fi
echo "::error::${label} did not record a child run id."
return 1
fi
local status conclusion url attempt
status="$(gh run view "$run_id" --json status --jq '.status')"
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
attempt="$(gh run view "$run_id" --json attempt --jq '.attempt')"
echo "${label}: ${status}/${conclusion} attempt ${attempt}: ${url}"
if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then
echo "::error::${label} child run ended with ${status}/${conclusion}: ${url}"
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, status, conclusion, url}' || true
return 1
fi
}
summarize_child_timing() {
local label="$1"
local run_id="$2"
if [[ -z "${run_id// }" ]]; then
return 0
fi
{
echo
echo "### Slowest jobs: ${label}"
echo
gh run view "$run_id" --json jobs --jq '
def ts: fromdateiso8601;
"| Job | Result | Minutes |",
"| --- | --- | ---: |",
([.jobs[]
| select(.startedAt != "0001-01-01T00:00:00Z" and .completedAt != "0001-01-01T00:00:00Z")
| . + {durationMin: ((((.completedAt | ts) - (.startedAt | ts)) / 60) * 10 | round / 10)}
| {name, conclusion, durationMin}]
| sort_by(.durationMin)
| reverse
| .[0:10]
| map("| `" + (.name | gsub("\\|"; "\\|")) + "` | `" + ((.conclusion // "") | tostring) + "` | " + (.durationMin | tostring) + " |")
| .[])
' || echo "_Unable to summarize jobs for run ${run_id}._"
} >> "$GITHUB_STEP_SUMMARY"
}
failed=0
if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then
check_child "normal_ci" "" 0 || failed=1
else
check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1
fi
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then
check_child "release_checks" "" 0 || failed=1
else
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
fi
if [[ "$NPM_TELEGRAM_RESULT" == "skipped" && -z "${NPM_TELEGRAM_RUN_ID// }" ]]; then
check_child "npm_telegram" "" 0 || failed=1
else
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
fi
summarize_child_timing "normal_ci" "$NORMAL_CI_RUN_ID"
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
summarize_child_timing "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
exit "$failed"

View File

@@ -10,6 +10,11 @@ on:
required: false
default: false
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
workflow_call:
inputs:
ref:
@@ -21,6 +26,11 @@ on:
required: false
default: true
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
permissions:
contents: read
@@ -103,7 +113,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -114,7 +123,21 @@ jobs:
- name: Run root Dockerfile CLI smoke
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
const fs = require(\"node:fs\");
const path = require(\"node:path\");
const pkg = require(\"/app/package.json\");
for (const [dep, rel] of Object.entries(pkg.pnpm?.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(`missing patch for ${dep}: ${rel}`);
}
}
"
'
- name: Run agents delete shared workspace Docker CLI smoke
env:
@@ -204,7 +227,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -318,7 +340,7 @@ jobs:
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: latest
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh

View File

@@ -4,10 +4,25 @@ on:
workflow_dispatch:
inputs:
package_spec:
description: Published OpenClaw package spec to test
description: Published OpenClaw package spec to test when no artifact is supplied
required: true
default: openclaw@beta
type: string
package_label:
description: Optional display label for an artifact-backed package candidate
required: false
default: ""
type: string
package_artifact_name:
description: Advanced package-under-test artifact name; leave blank for registry install
required: false
default: ""
type: string
harness_ref:
description: Source ref for the private QA harness; defaults to the dispatched workflow ref
required: false
default: ""
type: string
provider_mode:
description: QA provider mode
required: true
@@ -20,6 +35,44 @@ on:
description: Optional comma-separated Telegram scenario ids
required: false
type: string
workflow_call:
inputs:
package_spec:
description: Published OpenClaw package spec to test when no artifact is supplied
required: true
type: string
package_artifact_name:
description: Optional package-under-test artifact from the current workflow run
required: false
default: ""
type: string
package_label:
description: Optional display label for an artifact-backed package candidate
required: false
default: ""
type: string
harness_ref:
description: Source ref for the private QA harness; defaults to the called workflow ref
required: false
default: ""
type: string
provider_mode:
description: QA provider mode
required: false
default: mock-openai
type: string
scenario:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
OPENCLAW_QA_CONVEX_SITE_URL:
required: false
OPENCLAW_QA_CONVEX_SECRET_CI:
required: false
permissions:
contents: read
@@ -34,106 +87,39 @@ env:
PNPM_VERSION: "10.33.0"
jobs:
validate_dispatch_ref:
name: Validate dispatch ref
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Require main workflow ref
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "NPM Telegram beta E2E must be dispatched from main so workflow logic stays controlled." >&2
exit 1
fi
approve_release_manager:
name: Approve npm Telegram beta E2E
needs: validate_dispatch_ref
runs-on: ubuntu-latest
environment: npm-release
steps:
- name: Record approval
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
run: echo "Approved npm Telegram beta E2E for ${PACKAGE_SPEC}"
prepare_docker_e2e_image:
name: Prepare Docker E2E image
needs: validate_dispatch_ref
run_package_telegram_e2e:
name: Run package Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout main
- name: Checkout dispatch ref
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
ref: ${{ inputs.harness_ref || github.sha }}
fetch-depth: 1
- name: Resolve Docker E2E image tag
id: image
shell: bash
env:
SELECTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
max-cache-size-mb: 800000
- name: Build and push Docker E2E image
- name: Build Docker E2E image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
platforms: linux/amd64
tags: ${{ steps.image.outputs.image }}
tags: openclaw-docker-e2e:local
load: true
push: false
provenance: false
push: true
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
needs: [approve_release_manager, prepare_docker_e2e_image]
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: read
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -145,6 +131,7 @@ jobs:
- name: Validate inputs and secrets
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
@@ -153,10 +140,19 @@ jobs:
run: |
set -euo pipefail
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1
if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1
fi
fi
case "${PROVIDER_MODE}" in
mock-openai | live-frontier) ;;
*)
echo "provider_mode must be mock-openai or live-frontier; got: ${PROVIDER_MODE}" >&2
exit 1
;;
esac
require_var() {
local key="$1"
@@ -172,21 +168,31 @@ jobs:
require_var OPENAI_API_KEY
fi
- name: Run npm Telegram beta E2E
- name: Download package-under-test artifact
if: inputs.package_artifact_name != ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.package_artifact_name }}
path: .artifacts/telegram-package-under-test
- name: Run package Telegram E2E
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL: ${{ inputs.package_label }}
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
run: |
set -euo pipefail
@@ -194,6 +200,20 @@ jobs:
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}"
if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then
mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort)
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
echo "package artifact ${PACKAGE_ARTIFACT_NAME} must contain exactly one .tgz; found ${#package_tgzs[@]}" >&2
exit 1
fi
export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"
if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgzs[0]}")"
fi
elif [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}"
fi
if [[ -n "${INPUT_SCENARIO// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_SCENARIOS="${INPUT_SCENARIO}"
fi
@@ -205,6 +225,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn

View File

@@ -51,6 +51,31 @@ on:
required: false
default: ""
type: string
candidate_artifact_name:
description: Optional current-run artifact name containing the candidate OpenClaw tarball
required: false
default: ""
type: string
candidate_artifact_run_id:
description: Optional workflow run id for candidate_artifact_name
required: false
default: ""
type: string
candidate_file_name:
description: Optional candidate tarball file name inside candidate_artifact_name
required: false
default: ""
type: string
candidate_version:
description: Optional candidate OpenClaw package version
required: false
default: ""
type: string
candidate_source_sha:
description: Optional source SHA used to build the candidate tarball
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
@@ -90,6 +115,31 @@ on:
required: false
default: ""
type: string
candidate_artifact_name:
description: Optional current-run artifact name containing the candidate OpenClaw tarball
required: false
default: ""
type: string
candidate_artifact_run_id:
description: Optional workflow run id for candidate_artifact_name
required: false
default: ""
type: string
candidate_file_name:
description: Optional candidate tarball file name inside candidate_artifact_name
required: false
default: ""
type: string
candidate_version:
description: Optional candidate OpenClaw package version
required: false
default: ""
type: string
candidate_source_sha:
description: Optional source SHA used to build the candidate tarball
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
@@ -119,7 +169,7 @@ env:
jobs:
prepare:
runs-on: ubuntu-latest
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }}
baseline_spec: ${{ steps.baseline.outputs.value }}
@@ -260,6 +310,7 @@ jobs:
persist-credentials: false
- name: Checkout public source ref
if: inputs.candidate_artifact_name == ''
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
@@ -280,17 +331,64 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
- name: Build candidate artifact once
if: inputs.candidate_artifact_name == ''
env:
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare
run: |
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
--prepare-only \
--source-dir source \
--output-dir "${OUTPUT_DIR}"
- name: Download provided candidate artifact
if: inputs.candidate_artifact_name != ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.candidate_artifact_name }}
run-id: ${{ inputs.candidate_artifact_run_id || github.run_id }}
github-token: ${{ github.token }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
- name: Capture provided candidate artifact metadata
if: inputs.candidate_artifact_name != ''
env:
PACKAGE_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
INPUT_CANDIDATE_FILE_NAME: ${{ inputs.candidate_file_name }}
INPUT_CANDIDATE_VERSION: ${{ inputs.candidate_version }}
INPUT_CANDIDATE_SOURCE_SHA: ${{ inputs.candidate_source_sha }}
CANDIDATE_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/candidate.json
run: |
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const packageDir = process.env.PACKAGE_DIR;
const requestedFileName = process.env.INPUT_CANDIDATE_FILE_NAME.trim();
const files = fs.readdirSync(packageDir).filter((file) => file.endsWith(".tgz"));
const candidateFileName = requestedFileName || (files.length === 1 ? files[0] : "");
if (!candidateFileName) {
throw new Error(`Expected exactly one candidate .tgz in ${packageDir}; found ${files.length}.`);
}
if (!fs.existsSync(path.join(packageDir, candidateFileName))) {
throw new Error(`Provided candidate artifact does not contain ${candidateFileName}.`);
}
const candidateVersion = process.env.INPUT_CANDIDATE_VERSION.trim();
if (!candidateVersion) {
throw new Error("candidate_version is required when candidate_artifact_name is provided.");
}
const sourceSha = process.env.INPUT_CANDIDATE_SOURCE_SHA.trim();
if (!/^[0-9a-f]{40}$/iu.test(sourceSha)) {
throw new Error("candidate_source_sha must be a full commit SHA when candidate_artifact_name is provided.");
}
fs.writeFileSync(
process.env.CANDIDATE_JSON,
`${JSON.stringify({ candidateFileName, candidateVersion, sourceSha }, null, 2)}\n`,
);
NODE
- name: Resolve baseline package spec
if: ${{ inputs.mode != 'fresh' }}
id: baseline
@@ -370,7 +468,7 @@ jobs:
VAR_WINDOWS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER }}
VAR_MACOS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER }}
run: |
MATRIX_JSON="$(pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
MATRIX_JSON="$(bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
--resolve-matrix \
--ref "${INPUT_REF}" \
--mode "${INPUT_MODE}" \
@@ -448,7 +546,7 @@ jobs:
if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then
DISCORD_ARGS+=(--run-discord-roundtrip true)
fi
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
--candidate-tgz "${CANDIDATE_TGZ}" \
--candidate-version "${CANDIDATE_VERSION}" \
--source-sha "${SOURCE_SHA}" \

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,14 @@ on:
workflow_dispatch:
inputs:
ref:
description: Existing release tag or current full 40-character workflow-branch commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567)
description: Branch, tag, or full commit SHA to validate
required: true
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
@@ -25,6 +30,29 @@ on:
- fresh
- upgrade
- both
release_profile:
description: Release coverage profile for live/Docker/provider breadth
required: false
default: full
type: choice
options:
- minimum
- stable
- full
rerun_group:
description: Release check group to run
required: false
default: all
type: choice
options:
- all
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
concurrency:
group: openclaw-release-checks-${{ inputs.ref }}
@@ -47,6 +75,8 @@ jobs:
sha: ${{ steps.ref.outputs.sha }}
provider: ${{ steps.inputs.outputs.provider }}
mode: ${{ steps.inputs.outputs.mode }}
release_profile: ${{ steps.inputs.outputs.release_profile }}
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
steps:
- name: Require main or release workflow ref for release checks
env:
@@ -61,89 +91,230 @@ jobs:
- name: Validate ref input
env:
RELEASE_REF: ${{ inputs.ref }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Expected an existing release tag or current full 40-character workflow-branch commit SHA, got: ${RELEASE_REF}" >&2
if [[ -z "${RELEASE_REF// }" ]] || [[ "${RELEASE_REF}" == -* ]]; then
echo "Expected a branch, tag, or full commit SHA; got: ${RELEASE_REF}" >&2
exit 1
fi
if [[ -n "${EXPECTED_SHA// }" ]] && [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Expected expected_sha to be a full commit SHA; got: ${EXPECTED_SHA}" >&2
exit 1
fi
- name: Checkout selected ref
- name: Checkout trusted workflow helper
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
path: workflow
fetch-depth: 1
- name: Fast-resolve selected ref
id: fast_ref
env:
RELEASE_REF: ${{ inputs.ref }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
bash workflow/scripts/github/resolve-openclaw-ref.sh \
--ref "$RELEASE_REF" \
--expected-sha "$EXPECTED_SHA" \
--fallback-ok \
--github-output "$GITHUB_OUTPUT"
- name: Checkout selected ref for reachability fallback
if: steps.fast_ref.outputs.fallback == 'true'
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
- name: Resolve checked-out SHA
id: ref
- name: Resolve checked-out fallback SHA
if: steps.fast_ref.outputs.fallback == 'true'
id: fallback_ref
working-directory: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate selected ref is on workflow branch
- name: Validate selected ref belongs to this repository
if: steps.fast_ref.outputs.fallback == 'true'
working-directory: source
env:
RELEASE_REF: ${{ inputs.ref }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
RELEASE_BRANCH_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}"
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
BRANCH_SHA="$(git rev-parse "${RELEASE_BRANCH_REF}")"
if [[ "$(git rev-parse HEAD)" != "${BRANCH_SHA}" ]]; then
echo "Commit SHA mode only supports the current ${WORKFLOW_REF_NAME} HEAD. Use a release tag for older commits." >&2
exit 1
fi
else
git merge-base --is-ancestor HEAD "${RELEASE_BRANCH_REF}"
SELECTED_SHA="$(git rev-parse HEAD)"
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git fetch --tags origin '+refs/tags/*:refs/tags/*'
if git tag --points-at "${SELECTED_SHA}" | grep -Eq '^v'; then
exit 0
fi
if git for-each-ref --format='%(refname:short)' --contains "${SELECTED_SHA}" refs/remotes/origin | grep -Eq '^origin/'; then
exit 0
fi
echo "Ref '${RELEASE_REF}' resolved to ${SELECTED_SHA}, but that commit is not reachable from an OpenClaw branch or release tag." >&2
echo "Secret-bearing release checks only run repository-owned branch/tag history, not arbitrary unreferenced commits." >&2
exit 1
- name: Finalize resolved SHA
id: ref
env:
FAST_SHA: ${{ steps.fast_ref.outputs.sha }}
FALLBACK_SHA: ${{ steps.fallback_ref.outputs.sha }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
USED_FALLBACK: ${{ steps.fast_ref.outputs.fallback }}
run: |
set -euo pipefail
selected_sha="$FAST_SHA"
if [[ "$USED_FALLBACK" == "true" ]]; then
selected_sha="$FALLBACK_SHA"
fi
if [[ -z "$selected_sha" ]]; then
echo "Failed to resolve selected ref SHA." >&2
exit 1
fi
if [[ -n "${EXPECTED_SHA// }" ]] && [[ "${selected_sha,,}" != "${EXPECTED_SHA,,}" ]]; then
echo "Ref resolved to ${selected_sha}, expected ${EXPECTED_SHA}." >&2
exit 1
fi
echo "sha=${selected_sha,,}" >> "$GITHUB_OUTPUT"
- name: Capture selected inputs
id: inputs
env:
RELEASE_REF_INPUT: ${{ inputs.ref }}
RELEASE_PROVIDER_INPUT: ${{ inputs.provider }}
RELEASE_MODE_INPUT: ${{ inputs.mode }}
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
run: |
set -euo pipefail
{
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT"
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
env:
RELEASE_REF: ${{ inputs.ref }}
RELEASE_SHA: ${{ steps.ref.outputs.sha }}
RELEASE_REF_FAST_PATH: ${{ steps.fast_ref.outputs.fast }}
RELEASE_PROVIDER: ${{ inputs.provider }}
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
{
echo "## Release checks"
echo
echo "- Requested ref: \`${RELEASE_REF}\`"
echo "- Validated SHA: \`${RELEASE_SHA}\`"
echo "- Ref resolution fast path: \`${RELEASE_REF_FAST_PATH}\`"
echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`"
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
echo "- This run will execute cross-OS release validation, install smoke, QA Lab parity, Matrix, and Telegram lanes, and the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
} >> "$GITHUB_STEP_SUMMARY"
prepare_release_package:
name: Prepare release package artifact
needs: [resolve_target]
if: contains(fromJSON('["all","cross-os","live-e2e","package"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
outputs:
artifact_name: ${{ steps.artifact.outputs.name }}
package_sha256: ${{ steps.package.outputs.sha256 }}
package_version: ${{ steps.package.outputs.package_version }}
source_sha: ${{ steps.package.outputs.source_sha }}
steps:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
- name: Set artifact metadata
id: artifact
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "false"
- name: Resolve release package artifact
id: package
shell: bash
env:
PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }}
run: |
set -euo pipefail
node scripts/resolve-openclaw-package-candidate.mjs \
--source ref \
--package-ref "$PACKAGE_REF" \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz \
--metadata .artifacts/docker-e2e-package/package-candidate.json \
--github-output "$GITHUB_OUTPUT"
digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")"
version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")"
source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
{
echo "## Release package artifact"
echo
echo "- Artifact: \`release-package-under-test\`"
echo "- Package ref: \`$PACKAGE_REF\`"
echo "- SHA-256: \`$digest\`"
echo "- Version: \`$version\`"
echo "- Source SHA: \`$source_sha\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload release package artifact
uses: actions/upload-artifact@v7
with:
name: release-package-under-test
path: .artifacts/docker-e2e-package/openclaw-current.tgz
retention-days: 14
if-no-files-found: error
install_smoke_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","install-smoke"]'), needs.resolve_target.outputs.rerun_group)
permissions:
contents: read
uses: ./.github/workflows/install-smoke.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
ref: ${{ needs.resolve_target.outputs.sha }}
run_bun_global_install_smoke: true
cross_os_release_checks:
needs: [resolve_target]
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","cross-os"]'), needs.resolve_target.outputs.rerun_group)
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
provider: ${{ needs.resolve_target.outputs.provider }}
mode: ${{ needs.resolve_target.outputs.mode }}
candidate_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
candidate_artifact_run_id: ${{ github.run_id }}
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 }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -153,18 +324,23 @@ jobs:
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
live_and_e2e_release_checks:
needs: [resolve_target]
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","live-e2e"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
ref: ${{ needs.resolve_target.outputs.sha }}
include_repo_e2e: true
include_release_path_suites: true
include_openwebui: true
include_openwebui: ${{ needs.resolve_target.outputs.release_profile != 'minimum' }}
include_live_suites: true
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_artifact_run_id: ${{ github.run_id }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -173,6 +349,7 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
@@ -211,13 +388,91 @@ jobs:
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
qa_lab_parity_release_checks:
name: Run QA Lab parity gate
package_acceptance_release_checks:
name: Run package acceptance
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","package"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/package-acceptance.yml
with:
workflow_ref: ${{ github.ref_name }}
source: artifact
artifact_run_id: ${{ github.run_id }}
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
suite_profile: custom
docker_lanes: bundled-channel-deps-compat plugins-offline
telegram_mode: mock-openai
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
qa_lab_parity_lane_release_checks:
name: Run QA Lab parity lane (${{ matrix.lane }})
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- lane: candidate
output_dir: gpt54
- lane: baseline
output_dir: opus46
env:
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
@@ -233,7 +488,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_target.outputs.ref }}
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -246,25 +501,80 @@ jobs:
- name: Build private QA runtime
run: pnpm build
- name: Run OpenAI candidate lane
- name: Run parity lane
env:
QA_PARITY_LANE: ${{ matrix.lane }}
QA_PARITY_OUTPUT_DIR: ${{ matrix.output_dir }}
run: |
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.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
set -euo pipefail
case "${QA_PARITY_LANE}" in
candidate)
model="${OPENCLAW_CI_OPENAI_MODEL}"
alt_model="openai/gpt-5.4-alt"
;;
baseline)
model="anthropic/claude-opus-4-6"
alt_model="anthropic/claude-sonnet-4-6"
;;
*)
echo "Unknown QA parity lane: ${QA_PARITY_LANE}" >&2
exit 1
;;
esac
- name: Run Opus 4.6 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-6 \
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/opus46
--model "${model}" \
--alt-model "${alt_model}" \
--output-dir ".artifacts/qa-e2e/${QA_PARITY_OUTPUT_DIR}"
- name: Upload parity lane artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.sha }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
qa_lab_parity_report_release_checks:
name: Run QA Lab parity report
needs: [resolve_target, qa_lab_parity_lane_release_checks]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 20
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:
ref: ${{ needs.resolve_target.outputs.sha }}
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 parity lane artifacts
uses: actions/download-artifact@v4
with:
pattern: release-qa-parity-*-${{ needs.resolve_target.outputs.sha }}
path: .artifacts/qa-e2e/
merge-multiple: true
- name: Build private QA runtime
run: pnpm build
- name: Generate parity report
run: |
@@ -288,6 +598,7 @@ jobs:
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
@@ -301,7 +612,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_target.outputs.ref }}
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -332,32 +643,41 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
pnpm openclaw qa matrix \
matrix_args=(
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--profile fast \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.sha }}
path: ${{ steps.run_lane.outputs.output_dir }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
qa_live_telegram_release_checks:
name: Run QA Lab live Telegram lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
@@ -371,7 +691,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_target.outputs.ref }}
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -435,6 +755,48 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.sha }}
path: ${{ steps.run_lane.outputs.output_dir }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
summary:
name: Verify release checks
needs:
- prepare_release_package
- install_smoke_release_checks
- cross_os_release_checks
- live_and_e2e_release_checks
- package_acceptance_release_checks
- qa_lab_parity_lane_release_checks
- qa_lab_parity_report_release_checks
- qa_live_matrix_release_checks
- qa_live_telegram_release_checks
if: always()
runs-on: ubuntu-24.04
permissions: {}
timeout-minutes: 5
steps:
- name: Verify release check results
shell: bash
run: |
set -euo pipefail
failed=0
for item in \
"prepare_release_package=${{ needs.prepare_release_package.result }}" \
"install_smoke_release_checks=${{ needs.install_smoke_release_checks.result }}" \
"cross_os_release_checks=${{ needs.cross_os_release_checks.result }}" \
"live_and_e2e_release_checks=${{ needs.live_and_e2e_release_checks.result }}" \
"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_live_matrix_release_checks=${{ needs.qa_live_matrix_release_checks.result }}" \
"qa_live_telegram_release_checks=${{ needs.qa_live_telegram_release_checks.result }}"
do
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "::error::${name} ended with ${result}"
failed=1
fi
done
exit "$failed"

View File

@@ -38,6 +38,7 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}

532
.github/workflows/package-acceptance.yml vendored Normal file
View File

@@ -0,0 +1,532 @@
name: Package Acceptance
on:
workflow_dispatch:
inputs:
workflow_ref:
description: Trusted repo ref for workflow scripts and Docker E2E harness
required: true
default: main
type: string
source:
description: Package candidate source
required: true
default: npm
type: choice
options:
- npm
- ref
- url
- artifact
package_ref:
description: Trusted package source ref when source=ref
required: true
default: main
type: string
package_spec:
description: Published package spec when source=npm
required: false
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
required: false
default: ""
type: string
artifact_run_id:
description: GitHub Actions run id when source=artifact
required: false
default: ""
type: string
artifact_name:
description: Artifact name containing one .tgz when source=artifact
required: false
default: package-under-test
type: string
suite_profile:
description: Acceptance profile
required: true
default: package
type: choice
options:
- smoke
- package
- product
- full
- custom
docker_lanes:
description: Comma/space separated Docker lanes when suite_profile=custom
required: false
default: ""
type: string
telegram_mode:
description: Optional Telegram QA lane for the resolved package candidate
required: true
default: none
type: choice
options:
- none
- mock-openai
- live-frontier
telegram_scenarios:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
workflow_call:
inputs:
workflow_ref:
description: Trusted repo ref for workflow scripts and Docker E2E harness
required: false
default: main
type: string
source:
description: "Package candidate source: npm, ref, url, or artifact"
required: true
type: string
package_ref:
description: Trusted package source ref when source=ref
required: false
default: main
type: string
package_spec:
description: Published package spec when source=npm
required: false
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
required: false
default: ""
type: string
artifact_run_id:
description: GitHub Actions run id when source=artifact
required: false
default: ""
type: string
artifact_name:
description: Artifact name containing one .tgz when source=artifact
required: false
default: package-under-test
type: string
suite_profile:
description: "Acceptance profile: smoke, package, product, full, or custom"
required: false
default: package
type: string
docker_lanes:
description: Comma/space separated Docker lanes when suite_profile=custom
required: false
default: ""
type: string
telegram_mode:
description: Optional Telegram QA lane for the resolved package candidate
required: false
default: none
type: string
telegram_scenarios:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
OPENAI_BASE_URL:
required: false
ANTHROPIC_API_KEY:
required: false
ANTHROPIC_API_KEY_OLD:
required: false
ANTHROPIC_API_TOKEN:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
required: false
DEEPINFRA_API_KEY:
required: false
DASHSCOPE_API_KEY:
required: false
GROQ_API_KEY:
required: false
KIMI_API_KEY:
required: false
MODELSTUDIO_API_KEY:
required: false
MOONSHOT_API_KEY:
required: false
MISTRAL_API_KEY:
required: false
MINIMAX_API_KEY:
required: false
OPENCODE_API_KEY:
required: false
OPENCODE_ZEN_API_KEY:
required: false
OPENCLAW_LIVE_BROWSER_CDP_URL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_MODEL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_VALUE:
required: false
GEMINI_API_KEY:
required: false
GOOGLE_API_KEY:
required: false
OPENROUTER_API_KEY:
required: false
QWEN_API_KEY:
required: false
FAL_KEY:
required: false
RUNWAY_API_KEY:
required: false
DEEPGRAM_API_KEY:
required: false
TOGETHER_API_KEY:
required: false
VYDRA_API_KEY:
required: false
XAI_API_KEY:
required: false
ZAI_API_KEY:
required: false
Z_AI_API_KEY:
required: false
BYTEPLUS_ACCESS_KEY_ID:
required: false
BYTEPLUS_SECRET_ACCESS_KEY:
required: false
CLAUDE_CODE_OAUTH_TOKEN:
required: false
OPENCLAW_CODEX_AUTH_JSON:
required: false
OPENCLAW_CODEX_CONFIG_TOML:
required: false
OPENCLAW_CLAUDE_JSON:
required: false
OPENCLAW_CLAUDE_CREDENTIALS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON:
required: false
OPENCLAW_GEMINI_SETTINGS_JSON:
required: false
FIREWORKS_API_KEY:
required: false
OPENCLAW_QA_CONVEX_SITE_URL:
required: false
OPENCLAW_QA_CONVEX_SECRET_CI:
required: false
permissions:
actions: read
contents: read
packages: write
pull-requests: read
concurrency:
group: package-acceptance-${{ github.run_id }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
PACKAGE_ARTIFACT_NAME: package-under-test
jobs:
resolve_package:
name: Resolve package candidate
runs-on: ubuntu-24.04
timeout-minutes: 60
outputs:
docker_lanes: ${{ steps.profile.outputs.docker_lanes }}
include_live_suites: ${{ steps.profile.outputs.include_live_suites }}
include_openwebui: ${{ steps.profile.outputs.include_openwebui }}
include_release_path_suites: ${{ steps.profile.outputs.include_release_path_suites }}
package_artifact_name: ${{ steps.profile.outputs.package_artifact_name }}
package_sha256: ${{ steps.resolve.outputs.sha256 }}
package_version: ${{ steps.resolve.outputs.package_version }}
telegram_enabled: ${{ steps.profile.outputs.telegram_enabled }}
telegram_mode: ${{ steps.profile.outputs.telegram_mode }}
steps:
- name: Checkout package workflow ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: ${{ inputs.source == 'ref' && 'true' || 'false' }}
install-deps: "false"
- name: Download package artifact input
if: inputs.source == 'artifact'
env:
GH_TOKEN: ${{ github.token }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
ARTIFACT_NAME: ${{ inputs.artifact_name }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${ARTIFACT_RUN_ID// }" ]]; then
echo "artifact_run_id is required when source=artifact." >&2
exit 1
fi
if [[ -z "${ARTIFACT_NAME// }" ]]; then
echo "artifact_name is required when source=artifact." >&2
exit 1
fi
mkdir -p .artifacts/package-candidate-input
gh run download "$ARTIFACT_RUN_ID" -n "$ARTIFACT_NAME" -D .artifacts/package-candidate-input
- name: Resolve package candidate
id: resolve
env:
SOURCE: ${{ inputs.source }}
PACKAGE_REF: ${{ inputs.package_ref }}
PACKAGE_SPEC: ${{ inputs.package_spec }}
PACKAGE_URL: ${{ inputs.package_url }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
shell: bash
run: |
set -euo pipefail
artifact_dir=""
if [[ "$SOURCE" == "artifact" ]]; then
artifact_dir=".artifacts/package-candidate-input"
fi
node scripts/resolve-openclaw-package-candidate.mjs \
--source "$SOURCE" \
--package-ref "$PACKAGE_REF" \
--package-spec "$PACKAGE_SPEC" \
--package-url "$PACKAGE_URL" \
--package-sha256 "$PACKAGE_SHA256" \
--artifact-dir "${artifact_dir:-.}" \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz \
--metadata .artifacts/docker-e2e-package/package-candidate.json \
--github-output "$GITHUB_OUTPUT"
- name: Select acceptance profile
id: profile
env:
SOURCE: ${{ inputs.source }}
SUITE_PROFILE: ${{ inputs.suite_profile }}
CUSTOM_DOCKER_LANES: ${{ inputs.docker_lanes }}
TELEGRAM_MODE: ${{ inputs.telegram_mode }}
shell: bash
run: |
set -euo pipefail
include_release_path_suites=false
include_openwebui=false
include_live_suites=false
docker_lanes=""
case "$SUITE_PROFILE" in
smoke)
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
;;
package)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps-compat plugins-offline plugin-update"
;;
product)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps-compat plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
include_openwebui=true
;;
full)
include_release_path_suites=true
include_openwebui=true
;;
custom)
docker_lanes="$CUSTOM_DOCKER_LANES"
if [[ -z "${docker_lanes// }" ]]; then
echo "docker_lanes is required when suite_profile=custom." >&2
exit 1
fi
if [[ "$docker_lanes" == *"openwebui"* ]]; then
include_openwebui=true
fi
;;
*)
echo "Unknown suite_profile: $SUITE_PROFILE" >&2
exit 1
;;
esac
telegram_enabled=false
if [[ "$TELEGRAM_MODE" != "none" ]]; then
telegram_enabled=true
fi
{
echo "docker_lanes=$docker_lanes"
echo "include_release_path_suites=$include_release_path_suites"
echo "include_openwebui=$include_openwebui"
echo "include_live_suites=$include_live_suites"
echo "telegram_enabled=$telegram_enabled"
echo "telegram_mode=$TELEGRAM_MODE"
echo "package_artifact_name=${PACKAGE_ARTIFACT_NAME}"
} >> "$GITHUB_OUTPUT"
- name: Upload package-under-test artifact
uses: actions/upload-artifact@v7
with:
name: ${{ env.PACKAGE_ARTIFACT_NAME }}
path: |
.artifacts/docker-e2e-package/openclaw-current.tgz
.artifacts/docker-e2e-package/package-candidate.json
retention-days: 14
if-no-files-found: error
- name: Summarize package candidate
env:
PACKAGE_SHA256: ${{ steps.resolve.outputs.sha256 }}
PACKAGE_VERSION: ${{ steps.resolve.outputs.package_version }}
PACKAGE_REF: ${{ inputs.package_ref }}
SOURCE: ${{ inputs.source }}
SUITE_PROFILE: ${{ inputs.suite_profile }}
WORKFLOW_REF: ${{ inputs.workflow_ref }}
shell: bash
run: |
{
echo "## Package acceptance"
echo
echo "- Source: \`${SOURCE}\`"
echo "- Workflow ref: \`${WORKFLOW_REF}\`"
if [[ "${SOURCE}" == "ref" ]]; then
echo "- Package ref: \`${PACKAGE_REF}\`"
fi
echo "- Version: \`${PACKAGE_VERSION}\`"
echo "- SHA-256: \`${PACKAGE_SHA256}\`"
echo "- Profile: \`${SUITE_PROFILE}\`"
} >> "$GITHUB_STEP_SUMMARY"
docker_acceptance:
name: Docker product acceptance
needs: resolve_package
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ inputs.workflow_ref }}
include_repo_e2e: false
include_release_path_suites: ${{ needs.resolve_package.outputs.include_release_path_suites == 'true' }}
include_openwebui: ${{ needs.resolve_package.outputs.include_openwebui == 'true' }}
docker_lanes: ${{ needs.resolve_package.outputs.docker_lanes }}
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
include_live_suites: ${{ needs.resolve_package.outputs.include_live_suites == 'true' }}
live_models_only: false
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
package_telegram:
name: Telegram package acceptance
needs: resolve_package
if: needs.resolve_package.outputs.telegram_enabled == 'true'
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
with:
package_spec: ${{ inputs.package_spec }}
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}
harness_ref: ${{ inputs.source == 'ref' && inputs.package_ref || inputs.workflow_ref }}
provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }}
scenario: ${{ inputs.telegram_scenarios }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
summary:
name: Verify package acceptance
needs: [resolve_package, docker_acceptance, package_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify package acceptance results
env:
DOCKER_RESULT: ${{ needs.docker_acceptance.result }}
PACKAGE_TELEGRAM_RESULT: ${{ needs.package_telegram.result }}
RESOLVE_RESULT: ${{ needs.resolve_package.result }}
shell: bash
run: |
set -euo pipefail
failed=0
for item in \
"resolve_package=${RESOLVE_RESULT}" \
"docker_acceptance=${DOCKER_RESULT}" \
"package_telegram=${PACKAGE_TELEGRAM_RESULT}"
do
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "::error::${name} ended with ${result}"
failed=1
fi
done
exit "$failed"

View File

@@ -18,6 +18,19 @@ on:
description: Optional comma-separated Discord scenario ids
required: false
type: string
matrix_profile:
description: Matrix QA profile for the live Matrix lane
required: false
default: all
type: choice
options:
- fast
- all
- transport
- media
- e2ee-smoke
- e2ee-deep
- e2ee-cli
permissions:
contents: read
@@ -199,6 +212,7 @@ jobs:
run_live_matrix:
name: Run Matrix live QA lane
needs: [authorize_actor, validate_selected_ref]
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all') }}
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
@@ -236,20 +250,29 @@ jobs:
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INPUT_MATRIX_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile || 'fast' }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
pnpm openclaw qa matrix \
matrix_args=(
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--profile "${INPUT_MATRIX_PROFILE}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA artifacts
if: always()
@@ -260,6 +283,88 @@ jobs:
retention-days: 14
if-no-files-found: warn
run_live_matrix_sharded:
name: Run Matrix live QA lane (${{ matrix.profile }})
needs: [authorize_actor, validate_selected_ref]
if: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all' }}
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
strategy:
fail-fast: false
matrix:
profile:
- transport
- media
- e2ee-smoke
- e2ee-deep
- e2ee-cli
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
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
run: pnpm build
- name: Run Matrix live lane shard
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${{ matrix.profile }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--profile "${{ matrix.profile }}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA shard artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: qa-live-matrix-${{ matrix.profile }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
run_live_telegram:
name: Run Telegram live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]

View File

@@ -29,7 +29,7 @@ jobs:
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests (primary)
- name: Mark stale unassigned issues and pull requests (primary)
id: stale-primary
continue-on-error: true
uses: actions/stale@v10
@@ -41,7 +41,7 @@ jobs:
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -56,11 +56,59 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (primary)
id: assigned-issue-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (primary)
id: assigned-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Check stale state cache
id: stale-state
@@ -86,7 +134,7 @@ jobs:
core.warning(`Failed to check stale state cache: ${message}`);
core.setOutput("has_state", "false");
}
- name: Mark stale issues and pull requests (fallback)
- name: Mark stale unassigned issues and pull requests (fallback)
if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
@@ -97,7 +145,7 @@ jobs:
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -112,11 +160,57 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (fallback)
if: (steps.assigned-issue-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (fallback)
if: (steps.assigned-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:

View File

@@ -181,7 +181,8 @@ jobs:
- name: Restore Node 24 path
if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
run:
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
set -euo pipefail
export PATH="${NODE_BIN}:${PATH}"
echo "${NODE_BIN}" >> "$GITHUB_PATH"

34
.gitignore vendored
View File

@@ -97,6 +97,40 @@ USER.md
# local tooling
.serena/
# Local project-agent skill installs. Only repo-owned skills are visible by
# default; promoting a new repo skill should require an intentional `git add -f`.
.agents/skills/*
!.agents/skills/blacksmith-testbox/
!.agents/skills/blacksmith-testbox/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/openclaw-ghsa-maintainer/
!.agents/skills/openclaw-ghsa-maintainer/**
!.agents/skills/openclaw-parallels-smoke/
!.agents/skills/openclaw-parallels-smoke/**
!.agents/skills/openclaw-pr-maintainer/
!.agents/skills/openclaw-pr-maintainer/**
!.agents/skills/openclaw-qa-testing/
!.agents/skills/openclaw-qa-testing/**
!.agents/skills/openclaw-release-maintainer/
!.agents/skills/openclaw-release-maintainer/**
!.agents/skills/openclaw-secret-scanning-maintainer/
!.agents/skills/openclaw-secret-scanning-maintainer/**
!.agents/skills/openclaw-test-heap-leaks/
!.agents/skills/openclaw-test-heap-leaks/**
!.agents/skills/openclaw-test-performance/
!.agents/skills/openclaw-test-performance/**
!.agents/skills/openclaw-testing/
!.agents/skills/openclaw-testing/**
!.agents/skills/optimizetests/
!.agents/skills/optimizetests/**
!.agents/skills/parallels-discord-roundtrip/
!.agents/skills/parallels-discord-roundtrip/**
!.agents/skills/security-triage/
!.agents/skills/security-triage/**
!.agents/skills/tag-duplicate-prs-issues/
!.agents/skills/tag-duplicate-prs-issues/**
# Agent credentials and memory (NEVER COMMIT)
/memory/
.agent/*.json

View File

@@ -29,6 +29,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Extension prod code: no core `src/**`, `src/plugin-sdk-internal/**`, other extension `src/**`, or relative outside package.
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use `api.ts`, SDK facade, generic contracts.
- Extension-owned behavior stays extension-owned: repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
@@ -49,15 +50,21 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Prod sweep: `pnpm check`; tests: `pnpm test`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; never raw `vitest`.
- Vitest flags only; no Jest flags like `--runInBand`. For serial runs use `pnpm test:serial` or `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test ...`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); do not add `tsc --noEmit`, `typecheck`, `check:types`.
- Format/lint: `pnpm format:check`/`pnpm format`; `pnpm lint*` lanes.
- Formatting: use `oxfmt`, not Prettier. Prefer `pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
- Blacksmith/Testbox: on maintainer machines with Blacksmith access, broad/shared validation defaults to Testbox. This includes `pnpm check`, `pnpm check:changed`, `pnpm test`, `pnpm test:changed`, Docker/E2E/live/package/build gates, and any command likely to fan out across many Vitest projects. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops only, such as `pnpm test <specific-file>`, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Testbox use: run from repo root, pre-warm early with `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`, reuse the returned `tbx_...` id for all `run`/`download` commands, and stop boxes you created before handoff. Timeout bins: `90` minutes default, `240` multi-hour, `720` all-day, `1440` overnight; anything above `1440` needs explicit approval and cleanup.
- Testbox full-suite profile: `blacksmith testbox run --id <ID> "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test"`. For installable package proof, prefer the GitHub `Package Acceptance` workflow over ad hoc Testbox commands.
## GitHub / CI
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
- PR scan/triage: no unsolicited PR comments/reviews. Report in chat only unless explicitly asked, or a close/duplicate action needs a reason comment.
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
@@ -85,7 +92,16 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- extension tests: extension test typecheck/tests
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Before handoff/push for code/test/runtime/config changes: run `pnpm check:changed` in Testbox by default on maintainer machines. Tests-only: run `pnpm test:changed` in Testbox by default. Full prod sweep: run `pnpm check` in Testbox. Use local only for narrow targeted proof or when explicitly requested.
- If `pnpm test:changed` or `pnpm check:changed` selects broad/shared lanes, it belongs in Testbox; do not let it continue locally after it fans out.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase
has no conflicts and the branch diff is materially unchanged. Do a quick
`git status`, `git diff --check`, and diff/stat sanity check; rerun targeted or
full checks only if conflict resolution, upstream overlap, generated drift,
dependency/config changes, or touched-file content changes make the prior
result stale.
- Landing on `main`: verify touched surface near landing. Default feasible bar: `pnpm check` + `pnpm test`.
- Hard build gate: `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
@@ -109,6 +125,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## Tests
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.4`.
- Avoid brittle tests that grep workflow/docs strings for operator policy. Prefer executable behavior, parsed config/schema checks, or live run proof; put release/CI policy reminders in AGENTS/docs instead.
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
- Hot tests: avoid per-test `vi.resetModules()` + heavy imports. Measure with `pnpm test:perf:imports <file>` / `pnpm test:perf:hotspots --limit N`.
- Seam depth: pure helper/contract unit tests; one integration smoke per boundary.
@@ -116,6 +133,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Prefer injection; if module mocking, mock narrow local `*.runtime.ts`, not broad barrels or `openclaw/plugin-sdk/*`.
- Share fixtures/builders; delete duplicate assertions; assert behavior that can regress here.
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
- Do not run multiple independent `pnpm test`/Vitest commands concurrently in the same worktree. They can race on `node_modules/.experimental-vitest-cache` and fail with `ENOTEMPTY`. Use one grouped `pnpm test ...` invocation, run targeted lanes sequentially, or set distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values when true parallel Vitest processes are needed.
- Test workers max 16. Memory pressure: `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live: `OPENCLAW_LIVE_TEST=1 pnpm test:live`; verbose `OPENCLAW_LIVE_TEST_QUIET=0`.
- Guide: `docs/help/testing.md`.
@@ -124,14 +142,17 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
- Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete`.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @codex`, `Thanks @openclaw`, or `Thanks @steipete`.
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
## Git
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only. It formats staged files; still run gates.
- Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push.
- `main`: no merge commits; rebase on latest `origin/main` before push. Do not
keep chasing `main` with repeated full gates after one green run plus a clean
rebase sanity pass.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@ Welcome to the lobster tank! 🦞
- **Tengji (George) Zhang** - Chinese model APIs, cloud, pi
- GitHub: [@odysseus0](https://github.com/odysseus0) · X: [@odysseus0z](https://x.com/odysseus0z)
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Dingtalk, Feishu
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Yuanbao, Dingtalk, Feishu
- GitHub: [@sliverp](https://github.com/sliverp) · X: [@sliver01234](https://x.com/sliver01234)
- **Mason Huang** - Stability, Security, Speed

View File

@@ -9,22 +9,19 @@
# bundled plugin workspace tree, so the main build layer is not invalidated by
# unrelated plugin source changes.
#
# Two runtime variants:
# Default (bookworm): docker build .
# Slim (bookworm-slim): docker build --build-arg OPENCLAW_VARIANT=slim .
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_VARIANT=default
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_DOCKER_APT_UPGRADE=1
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_DIGEST="sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
# Base images are pinned to SHA256 digests for reproducible builds.
# Trade-off: digests must be updated manually when upstream tags move.
# To update, run: docker buildx imagetools inspect node:24-bookworm (or podman)
# and replace the digest below with the current multi-arch manifest list entry.
# Dependabot refreshes these blessed digests; release builds consume the
# reviewed base snapshot instead of mutating distro state on every build.
# To update, run: docker buildx imagetools inspect node:24-bookworm and
# node:24-bookworm-slim (or podman) and replace the digests below with the
# current multi-arch manifest list entries.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
ARG OPENCLAW_EXTENSIONS
@@ -75,10 +72,20 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Fail fast here if the Matrix native binding did not materialize after install.
RUN echo "==> Verifying critical native addons..." && \
# paths. Matrix's native downloader can hit transient release CDN errors while
# still exiting successfully, so retry the package downloader before failing.
RUN set -eux; \
echo "==> Verifying critical native addons..."; \
for attempt in 1 2 3 4 5; do \
if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \
exit 0; \
fi; \
echo "matrix-sdk-crypto native addon missing; retrying download (${attempt}/5)"; \
node /app/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js || true; \
sleep $((attempt * 2)); \
done; \
find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q . || \
(echo "ERROR: matrix-sdk-crypto native addon missing (pnpm install may have silently failed on this arch)" >&2 && exit 1)
(echo "ERROR: matrix-sdk-crypto native addon missing after retries" >&2 && exit 1)
COPY . .
@@ -125,22 +132,15 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default
ARG OPENCLAW_NODE_BOOKWORM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_DIGEST}"
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim
# ── Runtime base image ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm-slim" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST}"
# ── Stage 3: Runtime ────────────────────────────────────────────
FROM base-${OPENCLAW_VARIANT}
ARG OPENCLAW_VARIANT
FROM base-runtime
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_DOCKER_APT_UPGRADE
# OCI base-image metadata for downstream image consumers.
# If you change these annotations, also update:
@@ -155,24 +155,24 @@ LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
WORKDIR /app
# Install system utilities present in bookworm but missing in bookworm-slim.
# On the full bookworm image these are already installed (apt-get is a no-op).
# Smoke workflows can opt out of distro upgrades to cut repeated CI time while
# keeping the default runtime image behavior unchanged.
# Install runtime system utilities missing from bookworm-slim.
# `ca-certificates` ships in `bookworm` (full) but not in `bookworm-slim`,
# so it must be installed explicitly here. Without it `/etc/ssl/certs/`
# stays empty and every HTTPS outbound dies at TLS handshake with
# `error setting certificate file`.
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update && \
if [ "${OPENCLAW_DOCKER_APT_UPGRADE}" != "0" ]; then \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends; \
fi && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
procps hostname curl git lsof openssl
ca-certificates procps hostname curl git lsof openssl && \
update-ca-certificates
RUN chown node:node /app
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
COPY --from=runtime-assets --chown=node:node /app/package.json .
COPY --from=runtime-assets --chown=node:node /app/patches ./patches
COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
COPY --from=runtime-assets --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
@@ -258,6 +258,11 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
# Pre-create the default state dir so first-run Docker named volumes mounted
# here inherit node ownership instead of starting as root-owned state.
RUN install -d -m 0700 -o node -g node /home/node/.openclaw && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700'
ENV NODE_ENV=production
# Security hardening: Run as non-root user

View File

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -24,7 +24,6 @@ ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends ${PACKAGES}
RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm; fi

View File

@@ -1,31 +1,13 @@
{
"originHash" : "24a723309d7a0039d3df3051106f77ac1ed7068a02508e3a6804e41d757e6c72",
"originHash" : "e6910acc97de62dc423c0a391985c1c2f28207951e356081539abde41f9ffc72",
"pins" : [
{
"identity" : "commander",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/Commander.git",
"state" : {
"revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce",
"version" : "0.2.1"
}
},
{
"identity" : "elevenlabskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
"version" : "0.1.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
"revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b",
"version" : "0.2.2"
}
},
{
@@ -45,24 +27,6 @@
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
"version" : "0.99.0"
}
},
{
"identity" : "swiftui-math",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swiftui-math",
"state" : {
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
"version" : "0.1.0"
}
},
{
"identity" : "textual",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/textual",
"state" : {
"revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38",
"version" : "0.3.1"
}
}
],
"version" : 3

View File

@@ -13,7 +13,7 @@ let package = Package(
.executable(name: "swabble", targets: ["SwabbleCLI"]),
],
dependencies: [
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.1"),
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.2"),
.package(url: "https://github.com/apple/swift-testing", from: "0.99.0"),
],
targets: [
@@ -43,7 +43,6 @@ let package = Package(
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("SwiftTesting"),
]),
.testTarget(
name: "swabbleTests",

View File

@@ -45,6 +45,15 @@ extension AttributedString {
}
return ranges.compactMap { range in
guard #available(macOS 26.0, iOS 26.0, *) else {
return AttributedString(self[range].characters)
}
return self.sentenceWithAudioTimeRange(range)
}
}
@available(macOS 26.0, iOS 26.0, *)
private func sentenceWithAudioTimeRange(_ range: Range<AttributedString.Index>) -> AttributedString? {
let audioTimeRanges = self[range].runs.filter {
!String(self[$0.range].characters)
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@@ -57,6 +66,5 @@ extension AttributedString {
start: start,
end: end)
return AttributedString(self[range].characters, attributes: attributes)
}
}
}

View File

@@ -17,29 +17,35 @@ public enum OutputFormat: String {
case .txt:
return String(transcript.characters)
case .srt:
func format(_ timeInterval: TimeInterval) -> String {
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
let s = Int(timeInterval) % 60
let m = (Int(timeInterval) / 60) % 60
let h = Int(timeInterval) / 60 / 60
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
}
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
CMTimeRange,
String)? in
guard let timeRange = sentence.audioTimeRange else { return nil }
return (timeRange, String(sentence.characters))
}.enumerated().map { index, run in
let (timeRange, text) = run
return """
\(index + 1)
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
"""
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
guard #available(macOS 26.0, iOS 26.0, *) else { return "" }
return self.srtText(for: transcript, maxLength: maxLength)
}
}
@available(macOS 26.0, iOS 26.0, *)
private func srtText(for transcript: AttributedString, maxLength: Int) -> String {
func format(_ timeInterval: TimeInterval) -> String {
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
let s = Int(timeInterval) % 60
let m = (Int(timeInterval) / 60) % 60
let h = Int(timeInterval) / 60 / 60
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
}
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
CMTimeRange,
String)? in
guard let timeRange = sentence.audioTimeRange else { return nil }
return (timeRange, String(sentence.characters))
}.enumerated().map { index, run in
let (timeRange, text) = run
return """
\(index + 1)
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
"""
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@@ -13,7 +13,9 @@ public struct WakeWordSegment: Sendable, Equatable {
self.range = range
}
public var end: TimeInterval { start + duration }
public var end: TimeInterval {
self.start + self.duration
}
}
public struct WakeWordGateConfig: Sendable, Equatable {
@@ -24,7 +26,8 @@ public struct WakeWordGateConfig: Sendable, Equatable {
public init(
triggers: [String],
minPostTriggerGap: TimeInterval = 0.45,
minCommandLength: Int = 1) {
minCommandLength: Int = 1)
{
self.triggers = triggers
self.minPostTriggerGap = minPostTriggerGap
self.minCommandLength = minCommandLength
@@ -35,11 +38,18 @@ public struct WakeWordGateMatch: Sendable, Equatable {
public let triggerEndTime: TimeInterval
public let postGap: TimeInterval
public let command: String
public let trigger: String?
public init(triggerEndTime: TimeInterval, postGap: TimeInterval, command: String) {
public init(
triggerEndTime: TimeInterval,
postGap: TimeInterval,
command: String,
trigger: String? = nil)
{
self.triggerEndTime = triggerEndTime
self.postGap = postGap
self.command = command
self.trigger = trigger
}
}
@@ -53,13 +63,17 @@ public enum WakeWordGate {
}
private struct TriggerTokens {
let source: String
let tokens: [String]
}
private struct MatchCandidate {
let index: Int
let endIndex: Int
let tokenCount: Int
let triggerEnd: TimeInterval
let gap: TimeInterval
let trigger: String
}
public static func match(
@@ -67,10 +81,10 @@ public enum WakeWordGate {
segments: [WakeWordSegment],
config: WakeWordGateConfig)
-> WakeWordGateMatch? {
let triggerTokens = normalizeTriggers(config.triggers)
let triggerTokens = self.normalizeTriggers(config.triggers)
guard !triggerTokens.isEmpty else { return nil }
let tokens = normalizeSegments(segments)
let tokens = self.normalizeSegments(segments)
guard !tokens.isEmpty else { return nil }
var best: MatchCandidate?
@@ -87,17 +101,31 @@ public enum WakeWordGate {
let gap = nextToken.start - triggerEnd
if gap < config.minPostTriggerGap { continue }
if let best, i <= best.index { continue }
let endIndex = i + count - 1
if let best {
if endIndex < best.endIndex { continue }
if endIndex == best.endIndex, count <= best.tokenCount { continue }
}
best = MatchCandidate(index: i, triggerEnd: triggerEnd, gap: gap)
best = MatchCandidate(
index: i,
endIndex: endIndex,
tokenCount: count,
triggerEnd: triggerEnd,
gap: gap,
trigger: trigger.source)
}
}
guard let best else { return nil }
let command = commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
let command = self.commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
.trimmingCharacters(in: Self.whitespaceAndPunctuation)
guard command.count >= config.minCommandLength else { return nil }
return WakeWordGateMatch(triggerEndTime: best.triggerEnd, postGap: best.gap, command: command)
return WakeWordGateMatch(
triggerEndTime: best.triggerEnd,
postGap: best.gap,
command: command,
trigger: best.trigger)
}
public static func commandText(
@@ -120,7 +148,7 @@ public enum WakeWordGate {
guard !text.isEmpty else { return false }
let normalized = text.lowercased()
for trigger in triggers {
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation).lowercased()
let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation).lowercased()
if token.isEmpty { continue }
if normalized.contains(token) { return true }
}
@@ -130,11 +158,11 @@ public enum WakeWordGate {
public static func stripWake(text: String, triggers: [String]) -> String {
var out = text
for trigger in triggers {
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation)
let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation)
guard !token.isEmpty else { continue }
out = out.replacingOccurrences(of: token, with: "", options: [.caseInsensitive])
}
return out.trimmingCharacters(in: whitespaceAndPunctuation)
return out.trimmingCharacters(in: self.whitespaceAndPunctuation)
}
private static func normalizeTriggers(_ triggers: [String]) -> [TriggerTokens] {
@@ -142,17 +170,17 @@ public enum WakeWordGate {
for trigger in triggers {
let tokens = trigger
.split(whereSeparator: { $0.isWhitespace })
.map { normalizeToken(String($0)) }
.map { self.normalizeToken(String($0)) }
.filter { !$0.isEmpty }
if tokens.isEmpty { continue }
output.append(TriggerTokens(tokens: tokens))
output.append(TriggerTokens(source: tokens.joined(separator: " "), tokens: tokens))
}
return output
}
private static func normalizeSegments(_ segments: [WakeWordSegment]) -> [Token] {
segments.compactMap { segment in
let normalized = normalizeToken(segment.text)
let normalized = self.normalizeToken(segment.text)
guard !normalized.isEmpty else { return nil }
return Token(
normalized: normalized,
@@ -165,7 +193,7 @@ public enum WakeWordGate {
private static func normalizeToken(_ token: String) -> String {
token
.trimmingCharacters(in: whitespaceAndPunctuation)
.trimmingCharacters(in: self.whitespaceAndPunctuation)
.lowercased()
}

View File

@@ -5,6 +5,7 @@ import Speech
import Swabble
@MainActor
@available(macOS 26.0, *)
struct TranscribeCommand: ParsableCommand {
@Argument(help: "Path to audio/video file") var inputFile: String = ""
@Option(name: .long("locale"), help: "Locale identifier", parsing: .singleValue) var locale: String = Locale.current

View File

@@ -1,9 +1,9 @@
import Foundation
import SwabbleKit
import Testing
import XCTest
@Suite struct WakeWordGateTests {
@Test func matchRequiresGapAfterTrigger() {
final class WakeWordGateTests: XCTestCase {
func testMatchRequiresGapAfterTrigger() {
let transcript = "hey clawd do thing"
let segments = makeSegments(
transcript: transcript,
@@ -14,10 +14,10 @@ import Testing
("thing", 0.5, 0.1),
])
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
XCTAssertNil(WakeWordGate.match(transcript: transcript, segments: segments, config: config))
}
@Test func matchAllowsGapAndExtractsCommand() {
func testMatchAllowsGapAndExtractsCommand() {
let transcript = "hey clawd do thing"
let segments = makeSegments(
transcript: transcript,
@@ -29,10 +29,10 @@ import Testing
])
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
#expect(match?.command == "do thing")
XCTAssertEqual(match?.command, "do thing")
}
@Test func matchHandlesMultiWordTriggers() {
func testMatchHandlesMultiWordTriggers() {
let transcript = "hey clawd do it"
let segments = makeSegments(
transcript: transcript,
@@ -44,10 +44,25 @@ import Testing
])
let config = WakeWordGateConfig(triggers: ["hey clawd"], minPostTriggerGap: 0.3)
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
#expect(match?.command == "do it")
XCTAssertEqual(match?.command, "do it")
}
@Test func commandTextHandlesForeignRangeIndices() {
func testMatchPrefersMostSpecificTriggerWhenOverlapping() {
let transcript = "hey clawd do it"
let segments = makeSegments(
transcript: transcript,
words: [
("hey", 0.0, 0.1),
("clawd", 0.2, 0.1),
("do", 0.8, 0.1),
("it", 1.0, 0.1),
])
let config = WakeWordGateConfig(triggers: ["clawd", "hey clawd"], minPostTriggerGap: 0.3)
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
XCTAssertEqual(match?.trigger, "hey clawd")
}
func testCommandTextHandlesForeignRangeIndices() {
let transcript = "hey clawd do thing"
let other = "do thing"
let foreignRange = other.range(of: "do")
@@ -63,7 +78,7 @@ import Testing
segments: segments,
triggerEndTime: 0.3)
#expect(command == "do thing")
XCTAssertEqual(command, "do thing")
}
}

View File

@@ -1,23 +1,22 @@
import Foundation
import Testing
@testable import Swabble
import XCTest
@Test
func configRoundTrip() throws {
var cfg = SwabbleConfig()
cfg.wake.word = "robot"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json")
defer { try? FileManager.default.removeItem(at: url) }
final class ConfigTests: XCTestCase {
func testConfigRoundTrip() throws {
var cfg = SwabbleConfig()
cfg.wake.word = "robot"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json")
defer { try? FileManager.default.removeItem(at: url) }
try ConfigLoader.save(cfg, at: url)
let loaded = try ConfigLoader.load(at: url)
#expect(loaded.wake.word == "robot")
#expect(loaded.hook.prefix.contains("Voice swabble"))
}
try ConfigLoader.save(cfg, at: url)
let loaded = try ConfigLoader.load(at: url)
XCTAssertEqual(loaded.wake.word, "robot")
XCTAssertTrue(loaded.hook.prefix.contains("Voice swabble"))
}
@Test
func configMissingThrows() {
#expect(throws: ConfigError.missingConfig) {
_ = try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json"))
func testConfigMissingThrows() {
XCTAssertThrowsError(
try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json")))
}
}

View File

@@ -2,6 +2,645 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.26</title>
<pubDate>Tue, 28 Apr 2026 02:40:27 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042690</sparkle:version>
<sparkle:shortVersionString>2026.4.26</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.26</h2>
<h3>Changes</h3>
<ul>
<li>Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C <code>stream_messages</code> streaming with a <code>StreamingController</code> lifecycle manager, unified <code>sendMedia</code> with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via <code>createEngineAdapters()</code>. (#70624) Thanks @cxyhhhhh.</li>
<li>Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (<code>openclaw-plugin-yuanbao</code>) in the official channel catalog, contract suites, and community plugin docs, with a new <code>docs/channels/yuanbao.md</code> quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.</li>
<li>Control UI/Talk: add a generic browser realtime transport contract, Google Live browser Talk sessions with constrained ephemeral tokens, and a Gateway relay for backend-only realtime voice plugins. Thanks @VACInc.</li>
<li>CLI/models: route provider-filtered model listing through an explicit source plan so user config, installed manifest rows, Provider Index previews, and scoped runtime fallbacks keep a stable authority order without adding another catalog cache. Thanks @shakkernerd.</li>
<li>Providers: add Cerebras as a bundled plugin with onboarding, static model catalog, docs, and manifest-owned endpoint metadata.</li>
<li>Memory/OpenAI-compatible: add optional <code>memorySearch.inputType</code>, <code>queryInputType</code>, and <code>documentInputType</code> config for asymmetric embedding endpoints, including direct query embeddings and provider batch indexing. Carries forward #63313 and #60727. Thanks @HOYALIM and @prospect1314521.</li>
<li>Ollama/memory: add model-specific retrieval query prefixes for <code>nomic-embed-text</code>, <code>qwen3-embedding</code>, and <code>mxbai-embed-large</code> memory-search queries while leaving document batches unchanged. Carries forward #45013. Thanks @laolin5564.</li>
<li>Plugins/providers: move pre-runtime model-id normalization, endpoint host metadata, OpenAI-compatible request-family hints, model-catalog aliases/suppressions, OpenAI stale Spark suppression, and reusable startup metadata snapshots into plugin manifests so core no longer carries bundled-provider routing tables or repeated manifest rebuilds. Thanks @shakkernerd.</li>
<li>Plugins/config: deprecate direct plugin config load/write helpers in favor of passed runtime snapshots plus transactional mutation helpers with explicit restart follow-up policy, scanner guardrails, runtime warnings, and revision-based cache invalidation.</li>
<li>Plugins/install: allow <code>OPENCLAW_PLUGIN_STAGE_DIR</code> to contain layered runtime-dependency roots, resolving read-only preinstalled deps before installing missing deps into the final writable root. Fixes #72396. Thanks @liorb-mountapps.</li>
<li>Control UI: add a raw config pending-changes diff panel that parses JSON5, redacts sensitive values until reveal, and avoids fake raw-edit callbacks when opening the panel. Refs #39831; supersedes #48621 and #46654. Thanks @JiajunBernoulli and @BunsDev.</li>
<li>Control UI: polish the quick settings dashboard grid so common cards align across desktop, tablet, and mobile layouts without wasting horizontal space. Thanks @BunsDev.</li>
<li>Matrix/E2EE: add <code>openclaw matrix encryption setup</code> to enable Matrix encryption, bootstrap recovery, and print verification status from one setup flow. Thanks @gumadeiras.</li>
<li>Agents/compaction: add an opt-in <code>agents.defaults.compaction.maxActiveTranscriptBytes</code> preflight trigger that runs normal local compaction when the active JSONL grows too large, requiring transcript rotation so successful compaction moves future turns onto a smaller successor file instead of raw byte-splitting history. Thanks @vincentkoc.</li>
<li>CLI/migration: add <code>openclaw migrate</code> with plan, dry-run, JSON, pre-migration backup, onboarding detection, archive-only reports, a Claude Code/Desktop importer, and a Hermes importer for configuration, memory/plugin hints, model providers, MCP servers, skills, commands, and supported credentials. Thanks @vincentkoc and @NousResearch.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as <code>tsserver</code> do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.</li>
<li>Gateway/device tokens: stop echoing rotated bearer tokens from shared/admin <code>device.token.rotate</code> responses while preserving the same-device token handoff needed by token-only clients before reconnect. (#66773) Thanks @MoerAI.</li>
<li>Control UI/Talk: keep Google Live browser sessions on the WebSocket transport instead of falling back to WebRTC, validate browser Google Live WebSocket endpoints, cap Gateway relay sessions per browser connection, and remove stale browser-native voice buttons that did not use the configured Talk/TTS provider. Thanks @BunsDev.</li>
<li>Gateway/startup: reuse config snapshot plugin manifests for startup auto-enable, config validation, and plugin bootstrap planning, including authored source config and disabled setup-probe handling, so restrictive allowlists avoid duplicate manifest/config passes during boot. Thanks @shakkernerd.</li>
<li>Agents/subagents: enforce <code>subagents.allowAgents</code> for explicit same-agent <code>sessions_spawn(agentId=...)</code> calls instead of auto-allowing requester self-targets. Fixes #72827. Thanks @oiGaDio.</li>
<li>ACP/sessions_spawn: let explicit <code>sessions_spawn(runtime="acp")</code> bootstrap turns run while <code>acp.dispatch.enabled=false</code> still blocks automatic ACP thread dispatch. Fixes #63591. Thanks @moeedahmed.</li>
<li>CLI/update: install npm global updates into a verified temporary prefix before swapping the package tree into place, preventing mixed old/new installs and stale packaged files from breaking <code>openclaw update</code> verification. Thanks @shakkernerd.</li>
<li>Gateway: skip CLI startup self-respawn for foreground gateway runs so low-memory Linux/Node 24 hosts start through the same path as direct <code>dist/index.js</code> without hanging before logs. Fixes #72720. Thanks @sign-2025.</li>
<li>Google Meet: route local Chrome joins through OpenClaw browser control, grant Meet media permissions, pin local Chrome audio defaults to <code>BlackHole 2ch</code>, and use the configured OpenClaw browser profile so joined agents no longer show <code>Permission needed</code> or use raw/default Chrome state. Thanks @DougButdorf and @oromeis.</li>
<li>Plugins/discovery: follow symlinked plugin directories in global and workspace plugin roots while keeping broken links ignored and existing package safety checks in place. Fixes #36754; carries forward #72695 and #63206. Thanks @Quackstro, @ming1523, and @xsfX20.</li>
<li>Plugins/install: skip test files and directories during install security scans while still force-scanning declared runtime entrypoints, so packaged test mocks no longer block plugin installs. Fixes #66840; carries forward #67050. Thanks @saurabhjain1592 and @Magicray1217.</li>
<li>Plugins/install: allow exact package-manager peer links back to the trusted OpenClaw host package during install security scans while continuing to block spoofed or nested escaping <code>node_modules</code> symlinks. Carries forward #70819. Thanks @fgabelmannjr.</li>
<li>Plugins/install: resolve plugin install destinations from the active profile state dir across CLI, ClawHub, marketplace, local path, and channel setup installs, so <code>openclaw --profile <name> plugins install ...</code> no longer writes into the default profile. Fixes #69960; carries forward #69971. Thanks @FrancisLyman and @Sanjays2402.</li>
<li>Plugins/registry: suppress duplicate-plugin startup warnings when a tracked npm-installed plugin intentionally overrides the bundled plugin with the same id. Carries forward #48673. Thanks @abdushsk.</li>
<li>Plugins/startup: reuse canonical realpath lookups throughout each plugin discovery pass, including package and manifest boundary checks, so Windows npm-global startups no longer repeat expensive path resolution for the same plugin roots. Fixes #65733. Thanks @welfo-beo.</li>
<li>Gateway/proxy: pass <code>ALL_PROXY</code> / <code>all_proxy</code> into the global Undici env-proxy dispatcher and provider proxy-fetch helper while keeping SSRF trusted-proxy auto-upgrade on <code>HTTP_PROXY</code> / <code>HTTPS_PROXY</code> only, so gateway/provider calls honor all-proxy setups without weakening guarded fetches. Fixes #43821; carries forward #43919. Thanks @RickyTong1.</li>
<li>Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111.</li>
<li>Discord: persist routed model-picker overrides when the hidden <code>/model</code> dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129.</li>
<li>Nodes/CLI: add <code>openclaw nodes remove --node <id|name|ip></code> and <code>node.pair.remove</code> so stale gateway-owned node pairing records can be cleaned without hand-editing state files.</li>
<li>Gateway: include the connecting client and fresh presence version in the initial <code>hello-ok</code> snapshot, so clients no longer need a follow-up event before seeing themselves online.</li>
<li>Docker: install the CA certificate bundle in the slim runtime image so HTTPS calls from containerized gateways no longer fail TLS setup after the <code>bookworm-slim</code> base switch. Fixes #72787. Thanks @ryuhaneul.</li>
<li>Providers/OpenRouter: remove retired Hunter Alpha and Healer Alpha static catalog rows and disable proxy reasoning injection for stale Hunter Alpha configs, so replies are not hidden when OpenRouter returns answer text in reasoning fields. Fixes #43942. Thanks @EvanDataForge.</li>
<li>Providers/reasoning: let Groq and LM Studio declare provider-native reasoning effort values, so Qwen thinking models receive <code>none</code>/<code>default</code> or <code>off</code>/<code>on</code> instead of OpenAI-only <code>low</code>/<code>medium</code> values. Fixes #32638. Thanks @Aqu1bp, @mgoulart, @Norpps, and @BSTail.</li>
<li>Local models: default custom providers with only <code>baseUrl</code> to the Chat Completions adapter and trust loopback model requests automatically, so local OpenAI-compatible proxies receive <code>/v1/chat/completions</code> without timing out. Fixes #40024. Thanks @parachuteshe.</li>
<li>Channels/message tool: surface Discord, Slack, and Mattermost <code>user:</code>/<code>channel:</code> target syntax in the shared message target schema and Discord ambiguity errors, so DM sends by numeric id stop burning retries before finding <code>user:<id></code>. Fixes #72401. Thanks @garyd9, @hclsys, and @praveen9354.</li>
<li>Agents/tools: scope tool-loop detection history to the active run when available, so scheduled heartbeat cycles no longer inherit stale repeated-call counts from previous runs. Fixes #40144. Thanks @mattbrown319.</li>
<li>Agents/subagents: preserve requester delivery for completion announces across different channel accounts, keep same-channel thread completions routed to the child thread, and fail closed instead of guessing a child binding when requester conversation signal is missing. Thanks @sfuminya and @suyua9.</li>
<li>Agents/status: persist the post-compaction token estimate from auto-compaction when providers omit usage metadata, so <code>/status</code> and session lists keep showing fresh context usage after compaction. Fixes #67667; carries forward #72822. Thanks @Jimmy-xuzimo and @skylight-9.</li>
<li>Control UI: show loading, reload, and retry states when a lazy dashboard panel cannot load after an upgrade, so the Logs tab no longer appears blank on stale browser bundles. Fixes #72450. Thanks @sobergou.</li>
<li>Gateway/plugins: start the Gateway in degraded mode when a single plugin entry has invalid schema config, and let <code>openclaw doctor --fix</code> quarantine that plugin config instead of crash-looping every channel. Fixes #62976 and #70371. Thanks @Doraemon-Claw and @pksidekyk.</li>
<li>Agents/plugins: skip malformed plugin tools with missing schema objects and report plugin diagnostics, so one broken tool no longer crashes Anthropic agent runs. Fixes #69423. Thanks @jmnickels.</li>
<li>Agents/reasoning: recover fully wrapped unclosed <code><think></code> replies that would otherwise sanitize to empty text while keeping strict stripping for closed reasoning blocks and unclosed tails after visible text. Fixes #37696; supersedes #51915. Thanks @druide67 and @okuyam2y.</li>
<li>Control UI/Gateway: bind WebChat handshakes to their active socket and reject post-close server registrations, so aborted connects no longer leave zombie clients or misleading duplicate WebSocket connection logs. Fixes #72753. Thanks @LumenFromTheFuture.</li>
<li>Agents/fallback: split ambiguous provider failures into <code>empty_response</code>, <code>no_error_details</code>, and <code>unclassified</code>, and add flat fallback-step fields to structured fallback logs so primary-model failures stay visible when later fallbacks also fail. Fixes #71922; refs #71744. Thanks @andyk-ms and @nikolaykazakovvs-ux.</li>
<li>Plugins/Windows: normalize Windows absolute paths before handing bundled plugin modules to Jiti, so Feishu/Lark message sending no longer fails with unsupported <code>c:</code> ESM loader URLs. Fixes #72783. Thanks @jackychen-png.</li>
<li>CLI/doctor: run bundled plugin runtime-dependency repairs through the async npm installer with spinner/line progress and heartbeat updates, so long <code>openclaw doctor --fix</code> installs no longer look hung in TTY or piped output. Fixes #72775. Thanks @dfpalhano.</li>
<li>Feishu/Windows: normalize bundled channel sidecar loads before Jiti evaluates them, so Feishu outbound sends no longer fail with raw <code>C:</code> ESM loader errors on Windows. Fixes #72783. Thanks @jackychen-png.</li>
<li>Agents/tools: ignore volatile <code>exec</code> runtime metadata when comparing tool-loop outcomes, so enabled loop detection can stop repeated identical shell-command results instead of resetting on duration, PID, session, or cwd changes. Fixes #34574; supersedes #41502. Thanks @gucasbrg and @Zcg2021.</li>
<li>Agents/fallback: classify internal live-session model switch conflicts as unknown fallback failures instead of provider overloads, preventing local vLLM endpoints from receiving misleading overloaded cooldowns. Refs #63229. Thanks @clawdia-lobster.</li>
<li>Discord: let thread sessions inherit the parent channel's session-level <code>/model</code> override as a model-only fallback without enabling parent transcript inheritance. Fixes #72755. Thanks @solavrc.</li>
<li>Gateway/plugins: skip stale configured channels whose matching plugin is no longer discoverable, point cleanup at <code>openclaw doctor --fix</code>, and keep unrelated channel typos fatal so one missing channel plugin no longer crash-loops the Gateway. Fixes #53311. Thanks @futhgar.</li>
<li>Control UI: keep session-specific assistant identity loads authoritative after WebSocket connect, so non-main agent chat sessions do not show the main agent name in the header after bootstrap refreshes. Fixes #72776. Thanks @rockytian-top.</li>
<li>Agents/Qwen: preserve exact custom <code>modelstudio</code> provider configs with foreign <code>api</code> owners so explicit OpenAI-compatible Model Studio endpoints no longer get normalized into the bundled Qwen plugin path. Fixes #64483. Thanks @FiredMosquito831.</li>
<li>MCP/bundle-mcp: normalize CLI-native <code>type: "http"</code> MCP server entries to OpenClaw <code>transport: "streamable-http"</code> on save, repair existing configs with doctor, and keep embedded Pi from falling back to legacy SSE GET-first startup for those servers. Fixes #72757. Thanks @Studioscale.</li>
<li>OpenCode: expose Anthropic Opus/Sonnet 4.x thinking levels for proxied Claude models, so <code>/think xhigh</code>, <code>/think adaptive</code>, and <code>/think max</code> validate consistently with the direct Anthropic provider. Fixes #72729. Thanks @haishmg and @aaajiao.</li>
<li>Media-understanding/audio: migrate deprecated <code>{input}</code> placeholders in legacy <code>audio.transcription.command</code> configs to <code>{{MediaPath}}</code>, so custom audio transcribers no longer receive the literal placeholder after doctor repair. Fixes #72760. Thanks @krisfanue3-hash.</li>
<li>Ollama/WSL2: warn when GPU-backed WSL2 installs combine CUDA visibility with an autostarting <code>ollama.service</code> using <code>Restart=always</code>, and document the systemd, <code>.wslconfig</code>, and keep-alive mitigation for crash loops. Carries forward #61022; fixes #61185. Thanks @yhyatt.</li>
<li>Ollama/onboarding: de-dupe suggested bare local models against installed <code>:latest</code> tags and skip redundant pulls, so setup shows the installed model once and no longer says it is downloading an already available model. Fixes #68952. Thanks @tleyden.</li>
<li>Memory-core/doctor: keep <code>doctor.memory.status</code> on the cached path by default and only run live embedding pings for explicit deep probes, preventing slow local embedding backends from blocking Gateway status checks. Fixes #71568. Thanks @apex-system.</li>
<li>Memory/QMD: group same-source collections into one QMD search invocation when the installed QMD supports multiple <code>-c</code> filters, while keeping older QMD builds on the per-collection fallback. Fixes #72484; supersedes #72485 and #69583. Thanks @BsnizND and @zeroaltitude.</li>
<li>Memory/QMD: accept QMD status vector-count variants such as <code>Vectors = 42</code>, <code>Vectors:42</code>, and <code>Vectors: 42 embedded</code>, so <code>memory status --deep</code> no longer reports embeddings unavailable for healthy QMD wrappers. Fixes #63652; carries forward #63678. Thanks @apoapostolov and @WarrenJones.</li>
<li>Memory/QMD: skip QMD vector status probes and embedding maintenance in lexical <code>searchMode: "search"</code>, so BM25-only QMD setups on ARM do not trigger llama.cpp/Vulkan builds during status checks or embed cycles. Fixes #59234 and #67113. Thanks @PrinceOfEgypt, @Vksh07, @Snipe76, @NomLom, @t4r3e2q1-commits, and @dmak.</li>
<li>Memory/QMD: report the live watcher dirty state in memory status, so changed QMD-backed memory files show as dirty until the queued sync finishes. Fixes #60244. Thanks @xinzf.</li>
<li>Compaction: skip oversized pre-compaction checkpoint snapshots and prune duplicate long user turns from compaction input and rotated successor transcripts, preventing retry storms from being preserved across checkpoint cycles. Fixes #72780. Thanks @SweetSophia.</li>
<li>Control UI/Cron: render cron job prompts and run summaries as sanitized markdown in the dashboard, with full-width block content, safer link clicks, and no duplicate error text when a failed run has no summary. Supersedes #48504. Thanks @garethdaine.</li>
<li>Control UI/Gateway: preserve WebChat client version labels across localhost, 127.0.0.1, and IPv6 loopback aliases on the same port, avoiding misleading <code>vcontrol-ui</code> connection logs while investigating duplicate-message reports. Refs #72753 and #72742. Thanks @LumenFromTheFuture and @allesgutefy.</li>
<li>Agents/reasoning: treat orphan closing reasoning tags with following answer text as a privacy boundary across delivery, history, streaming, and Control UI sanitizers so malformed local-model output cannot leak chain-of-thought text. Fixes #67092. Thanks @AnildoSilva.</li>
<li>Memory-core: run one-shot memory CLI commands through transient builtin and QMD managers so <code>memory index</code>, <code>memory status --index</code>, and <code>memory search</code> no longer start long-lived file watchers that can hit macOS <code>EMFILE</code> limits. Fixes #59101; carries forward #49851. Thanks @mbear469210-coder and @maoyuanxue.</li>
<li>Agents/ACP: ship the Claude ACP adapter with OpenClaw and require Claude result messages before idle can complete a prompt, preventing parent agents from waking early on long-running <code>sessions_spawn(runtime: "acp", agentId: "claude")</code> children. Fixes #72080. Thanks @siavash-saki and @iannwu.</li>
<li>CLI/tasks: route <code>tasks --json</code>, <code>tasks list --json</code>, and <code>tasks audit --json</code> through a lean JSON path so read-only task inspection no longer loads unrelated plugin/runtime command graphs. Fixes #66238. Thanks @ChuckChambers.</li>
<li>Memory-core: re-resolve the active runtime config whenever <code>memory_search</code> or <code>memory_get</code> executes, so provider changes made by <code>config.patch</code> stop leaving stale embedding backends behind in existing tool instances. Fixes #61098. Thanks @BradGroux and @Linux2010.</li>
<li>WebChat: keep bare <code>/new</code> and <code>/reset</code> startup instructions out of visible chat history while preserving <code>/reset <note></code> as user-visible transcript text. Fixes #72369. Thanks @collynes and @haishmg.</li>
<li>Tasks/memory: checkpoint and truncate SQLite WAL sidecars on a timer and before close for task, Task Flow, proxy capture, and builtin memory databases, bounding long-running gateway <code>*.sqlite-wal</code> growth. Fixes #72774. Thanks @dfpalhano.</li>
<li>CLI/doctor: remove dangling channel config, heartbeat targets, and channel model overrides when stale plugin repair removes a missing channel plugin, preventing Gateway boot loops after failed plugin reinstalls. Fixes #65293. Thanks @yidecode.</li>
<li>Control UI/Gateway: cache, coalesce, stale-refresh, and invalidate effective tool inventory on channel registry changes while reusing the gateway-bound plugin registry and avoiding model/auth discovery, so chat runs no longer stall Control UI requests on repeated plugin/model setup. Fixes #72365; supersedes #72558. Thanks @Gabiii2398 and @1yihui.</li>
<li>Channels/setup: treat bundled channel plugins as already bundled during <code>channels add</code> and onboarding, enabling them without writing redundant <code>plugins.load.paths</code> entries or path install records. Fixes #72740. Thanks @iCodePoet.</li>
<li>WhatsApp: honor gateway <code>HTTPS_PROXY</code> / <code>HTTP_PROXY</code> env vars for QR-login WebSocket connections, while respecting <code>NO_PROXY</code>, so proxied networks no longer fall back to direct <code>mmg.whatsapp.net</code> connections that time out with 408. Fixes #72547; supersedes #72692. Thanks @mebusw and @SymbolStar.</li>
<li>Bonjour: default mDNS advertisements to the system hostname when it is DNS-safe, avoiding <code>openclaw.local</code> probing conflicts and Gateway restart loops on hosts such as <code>Lobster</code> or <code>ubuntu</code>. Fixes #72355 and #72689; supersedes #72694. Thanks @mscheuerlein-bot, @gcusms, @moyuwuhen601, @pavan987, @zml-0912, @hhq365, and @SymbolStar.</li>
<li>Agents/OpenAI-compatible: retry replay-safe empty <code>stop</code> turns once for <code>openai-completions</code> endpoints, so transient empty local backend responses no longer surface as “Agent couldn't generate a response” when a continuation succeeds, and restore <code>openclaw agent --model</code> for one-shot CLI runs. Fixes #72751. Thanks @moooV252.</li>
<li>Git hooks: skip ignored staged paths when formatting and restaging pre-commit files, so merge commits no longer abort when <code>.gitignore</code> newly ignores staged merged content. Fixes #72744. Thanks @100yenadmin.</li>
<li>Memory-core/dreaming: add a supported <code>dreaming.model</code> 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.</li>
<li>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.</li>
<li>Agents/vLLM/Qwen: add plugin-owned Qwen thinking controls for vLLM chat-template kwargs and DashScope-style top-level <code>enable_thinking</code> flags, including preserved thinking for agent loops. Fixes #72329. Thanks @stavrostzagadouris.</li>
<li>Memory-core/dreaming: treat request-scoped narrative fallback as expected, skip session cleanup when no subagent run was created, and remove duplicate phase-level cleanup so fallback no longer emits warning noise. Fixes #67152. Thanks @jsompis.</li>
<li>Agents/exec: apply configured <code>tools.exec.timeoutSec</code> to background, <code>yieldMs</code>, and node <code>system.run</code> commands when no per-call timeout is set, preventing auto-backgrounded and remote node commands from running indefinitely. Fixes #67600; supersedes #67603. Thanks @dlmpx and @kagura-agent.</li>
<li>Config/doctor: stop masking unknown-key validation diagnostics such as <code>agents.defaults.llm</code>, and have <code>openclaw doctor --fix</code> remove the retired <code>agents.defaults.llm</code> timeout block. Thanks @aidiffuser.</li>
<li>CLI/startup: keep the built pre-dispatch CLI graph free of package-level imports and extend packaged CLI smoke coverage to onboard and doctor help paths, preventing missing runtime dependencies such as tslog from killing onboarding before repair code can run. Fixes #63024. Thanks @hu19940121.</li>
<li>CLI/plugins: preserve unversioned ClawHub install specs so <code>plugins update</code> can follow newer ClawHub releases instead of pinning to the initially resolved version. Fixes #63010; supersedes #58426. Thanks @kangsen1234 and @robinspt.</li>
<li>Memory-core/subagents: tag plugin-created subagent sessions with their plugin owner so dreaming narrative cleanup can delete its own ephemeral sessions without granting broad admin session deletion. Fixes #72712. Thanks @BSG2000.</li>
<li>Gateway/models: move local-provider pricing opt-outs, OpenRouter/LiteLLM aliases, and proxy passthrough pricing lookup into plugin manifest metadata so core no longer carries extension-specific pricing tables.</li>
<li>CLI/update: honor <code>OPENCLAW_NO_AUTO_UPDATE=1</code> as a gateway startup kill-switch for configured background package auto-updates, so operators can hold a deliberate downgrade during incident recovery without editing config first. Fixes #72715. Thanks @Xivi08.</li>
<li>Agents/Claude CLI: force live-session launches to include <code>--output-format stream-json</code> whenever OpenClaw adds <code>--input-format stream-json</code>, so new Claude CLI sessions no longer fail immediately while reusable sessions keep working. Fixes #72206. Thanks @kwangwonkoh and @Xivi08.</li>
<li>CLI/plugins: accept ClawHub plugin API wildcard ranges such as <code>*</code> without rejecting compatible plugin installs, while still requiring a valid runtime API version. Fixes #56446; supersedes #56466. Thanks @darconada and @claygeo.</li>
<li>CLI/plugins: add an explicit <code>npm:<package></code> install prefix that skips ClawHub lookup for known npm packages while keeping bare package specs ClawHub-first. Fixes #55805; supersedes #54377. Thanks @Zeoy2020 and @vagusX.</li>
<li>CLI/plugins: let config-gated bundled plugins install without persisting invalid placeholder config entries, so install/uninstall sweeps can cover plugins such as memory-lancedb before the user configures credentials. Thanks @vincentkoc.</li>
<li>CLI/plugins: reject malformed ClawHub plugin specs with trailing <code>@</code> before registry lookup, so empty-version typos report as invalid specs instead of package-not-found errors. Fixes #56579; supersedes #56582. Thanks @Kansodata.</li>
<li>Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work.</li>
<li>Browser/plugins: auto-start the bundled browser plugin when root <code>browser</code> config is present, including restrictive plugin allowlists, and ignore stale persisted plugin registries whose package paths no longer exist.</li>
<li>Browser: circuit-break repeated managed Chrome launch failures per profile so browser requests stop spawning Chromium indefinitely when CDP cannot start. Fixes #64271. Thanks @TheophilusChinomona.</li>
<li>Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers.</li>
<li>CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody.</li>
<li>Agents/Anthropic: strip stale trailing assistant prefill turns from outbound replay so context-engine short circuits cannot send unsupported assistant-prefill payloads to provider APIs. Fixes #72556. Thanks @Veda-openclaw.</li>
<li>Agents/Google: strip stale trailing assistant/model prefill turns from Gemini outbound replay so Google Generative AI requests end with a user turn or function response. Follow-up to #72556. Thanks @Veda-openclaw.</li>
<li>Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1.</li>
<li>CLI/agent: mark Gateway-to-embedded fallback runs with <code>meta.transport: "embedded"</code> and <code>meta.fallbackFrom: "gateway"</code> in JSON output, and make the terminal diagnostic explicit so scripts and operators can distinguish fallback runs from Gateway runs. Fixes #71416. Thanks @amknight.</li>
<li>Agents/tools: normalize <code>null</code> or missing tool-call arguments to <code>{}</code> for parameterless object schemas before Pi validation, so empty-argument tools run instead of failing argument validation. Fixes #72587. Thanks @amknight.</li>
<li>Agents/subagents: clear active embedded-run state before terminal lifecycle events so post-completion cleanup no longer treats finished child runs as still active and skips archive or announcement bookkeeping. (#70187) Thanks @amknight.</li>
<li>CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding <code>.24</code> update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin.</li>
<li>Control UI: make explicit Reload Config actions discard stale local config edits while passive refreshes and failed-save recovery keep pending drafts intact. Fixes #40352; carries forward #40443. Thanks @realmikechong-dotcom.</li>
<li>Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock <code>ContentBlock</code> blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu.</li>
<li>Agents/LM Studio: promote standalone bracketed local-model tool requests into registered tool calls and hide unsupported bracket blocks from visible replies, so MemPalace MCP lookups do not print raw <code>[tool]</code> JSON scaffolding in chat. Fixes #66178. Thanks @detroit357.</li>
<li>Local models: warn when an assistant reply looks like a tool call but the provider emitted plain text instead of a structured tool invocation, making fake/non-executed tool calls visible in logs. Fixes #51332. Thanks @emilclaw.</li>
<li>Local models: accept persisted non-secret local auth markers for private-LAN custom OpenAI-compatible providers, so LAN Ollama configs no longer fail with missing auth when <code>ollama-local</code> is saved as the key. Fixes #49736. Thanks @charles-zh.</li>
<li>TUI/local models: treat visible gateway client labels such as <code>openclaw-tui</code> as the current requester session for session-aware tools, so Ollama tool calls no longer fail by resolving the UI label as a session id. Fixes #66391. Thanks @kickingzebra.</li>
<li>Local models: route self-hosted OpenAI-compatible model discovery through the guarded fetch path pinned to the configured host, covering vLLM and SGLang setup without reopening local/LAN SSRF probes. Supersedes #46359. Thanks @cdxiaodong.</li>
<li>Local models: classify terminated, reset, closed, timeout, and aborted model-call failures and attach a process memory snapshot to the diagnostic event, making LM Studio/Ollama RAM-pressure failures easier to prove from stability bundles. Refs #65551. Thanks @BigWiLLi111.</li>
<li>Local models: pass configured provider request timeouts through OpenAI SDK transports and the model idle watchdog so long-running local or custom OpenAI-compatible streams use one timeout knob instead of hitting the SDK's 10-minute default or the 120s idle default. Fixes #63663. Thanks @aidiffuser.</li>
<li>LM Studio: trust configured LM Studio loopback, LAN, and tailnet endpoints for guarded model requests by default, preserving explicit private-network opt-outs. Refs #60994. Thanks @tnowakow.</li>
<li>Docker/setup: route Docker onboarding defaults for host-side LM Studio and Ollama through <code>host.docker.internal</code> and add the Linux host-gateway mapping to the bundled Compose file, so containerized gateways can reach local providers without using container loopback. Fixes #68684; supersedes #68702. Thanks @safrano9999 and @skolez.</li>
<li>Agents/LM Studio: strip prior-turn Gemma 4 reasoning from OpenAI-compatible replay while preserving active tool-call continuation reasoning. Fixes #68704. Thanks @chip-snomo and @Kailigithub.</li>
<li>LM Studio: allow interactive onboarding to leave the API key blank for unauthenticated local servers, using local synthetic auth while clearing stale LM Studio auth profiles. Fixes #66937. Thanks @olamedia.</li>
<li>Plugins/startup/registry: reuse a Gateway <code>PluginLookUpTable</code> and one manifest registry pass across startup plugin IDs, plugin loading, deferred channel reloads, model pricing, read-only channel defaults, capability/provider/media resolution, manifest contracts, extractors, web fallback discovery, owner maps, and cold provider-discovery caches, with new startup-trace timing/count metrics for installed-index, manifest, startup-plan, and owner-map work. Thanks @shakkernerd and @mcaxtr.</li>
<li>Mattermost: keep direct-message replies top-level by suppressing reply roots for DM delivery while preserving channel and group thread roots, and derive inbound chat kind from the trusted channel lookup instead of the websocket event channel type. Carries forward #60115, #55186, #72305, and #72659; refs #59758, #59981, #59791, and #57565. Thanks @vincentkoc, @jwchmodx, and @hnykda.</li>
<li>Docker: pre-create <code>/home/node/.openclaw</code> with node ownership and private permissions so first-run Docker Compose named volumes no longer fail startup with EACCES. (#48072, #63959; fixes #61279) Thanks @timoxue and @jeanibarz.</li>
<li>CLI/Gateway: treat local restart probe policy closes for connect, exact <code>device required</code>, pairing, and auth failures as Gateway reachability proof without accepting empty, broad standalone token/password/scope/role, or pair-substring 1008 close reasons. Fixes #48771; carries forward #48801; related #63491. Thanks @MarsDoge and @genoooool.</li>
<li>Feishu: send outgoing interactive reply payloads as native cards with clickable buttons while preserving text, media, and document-comment fallbacks. Fixes #13175 and #58298; carries forward #47891. Thanks @Horacehxw.</li>
<li>Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear.</li>
<li>Bonjour/Windows: hide the bundled mDNS advertiser's Windows ARP shell probe so Gateway startup no longer flashes command-prompt windows. Fixes #70238. Thanks @alexandre-leng, @PratikRai0101, @infinitypacific, and @tomerpeled.</li>
<li>Agents/bootstrap: dedupe hook-injected bootstrap context files by workspace-relative path and store normalized resolved paths so duplicate relative and absolute hook paths no longer depend on the process cwd. (#59344; fixes #59319; related #56721, #56725, and #57587) Thanks @koen666.</li>
<li>Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when <code>AGENTS.md</code>, <code>SOUL.md</code>, <code>MEMORY.md</code>, or <code>TOOLS.md</code> change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon.</li>
<li>macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.</li>
<li>Plugins/install: treat mirrored core logger dependencies as staged bundled runtime deps so packaged Gateway starts do not crash when the external plugin-runtime-deps root is missing <code>tslog</code>. Fixes #72228; supersedes #72493. Thanks @deepujain.</li>
<li>Build/plugins: preserve active bundled runtime-dependency staging temp directories owned by live build processes so overlapping postbuild runs no longer delete each other's staged deps mid-prune. Supersedes #72220. Thanks @VACInc.</li>
<li>Plugins/install: hide bundled runtime-dependency npm child windows on Windows across Gateway startup, postinstall, and packaged staging paths so Telegram/Anthropic dependency repair no longer flashes shell windows. Fixes #72315. Thanks @athuljayaram and @joshfeng.</li>
<li>Agents/Windows: normalize lazy agent runtime imports before Node ESM loading so Windows drive-letter <code>subagent-registry</code> runtime paths no longer fail every agent task with <code>ERR_UNSUPPORTED_ESM_URL_SCHEME</code>. Fixes #72636; carries forward #72716. Thanks @Andyz-CData and @xialonglee.</li>
<li>Plugins/Windows: normalize lazy plugin service override imports before Node ESM loading so drive-letter browser-control module paths no longer fail with <code>ERR_UNSUPPORTED_ESM_URL_SCHEME</code>. Fixes #72573; supersedes #72599 and #72582. Thanks @llzzww316, @feineryonah-byte, and @WuKongAI-CMU.</li>
<li>Browser/plugins: load <code>playwright-core</code> through the browser runtime shim so packaged installs can run Playwright actions from staged plugin runtime deps after doctor/startup repair. Fixes #72168; supersedes #72238. Thanks @zdg1110 and @yetval.</li>
<li>Plugins/install: stage bundled plugin runtime dependencies before Gateway startup, drain update restarts, and materialize plugin-owned root chunks in external mirrors so staged deps resolve under native ESM. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss.</li>
<li>TTS/SecretRef: resolve <code>messages.tts.providers.*.apiKey</code> from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.</li>
<li>Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the target user scope when <code>systemctl --user</code> reports no-medium bus failures, without letting stale <code>SUDO_USER</code> override <code>sudo -u</code> installs. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, @mssteuer, and @boyuaner.</li>
<li>CLI/nodes: make unfiltered <code>openclaw nodes list</code> prefer the effective paired-node view used by <code>nodes status</code> while preserving pending rows, pairing-scope fallback, terminal-safe table rendering, and paired JSON metadata. Fixes #46871; carries forward #65772 through the ProjectClownfish #72619 repair. Thanks @skainguyen1412.</li>
<li>CLI/startup: read generated startup metadata from the bundled <code>dist</code> layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.</li>
<li>Feishu/Lark: stop treating broadcast-only <code>@all</code>/<code>@_all</code> messages as bot mentions while preserving direct bot mentions, including messages that also include <code>@all</code>. Fixes #37706. Thanks @JosepLee.</li>
<li>CLI/help: treat positional <code>help</code> invocations like <code>openclaw channels help</code> as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as <code>openclaw docs help</code>. Thanks @gumadeiras.</li>
<li>Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including <code>plugins.entries.brave.config.webSearch.apiKey</code>. Fixes #68690. Thanks @VACInc.</li>
<li>Voice Call: allow SecretRef-backed Twilio auth tokens and call-specific OpenAI/ElevenLabs TTS API keys through the plugin config surface. Fixes #68690. Thanks @joshavant.</li>
<li>Google Meet/Voice Call: clean stale chrome-node realtime bridges before rejoining, expose bridge inspection, tolerate transient node input pull failures, default Chrome command-pair audio to 24 kHz PCM16 while preserving legacy 8 kHz G.711 mu-law pairs, handle Gemini Live interruptions/VAD and function-response names correctly, route stateful <code>google_meet</code> tools through the gateway runtime, support <code>realtime.agentId</code>, and send non-blocking consult continuations before long tool-backed answers finish. Fixes #72371, #72525, #72523, #72440, and #72425; (#72372, #72524, #72381, #72441, #72189, #72426) Thanks @BsnizND and @VACInc.</li>
<li>Discord/media: keep incidental Markdown image badges in final replies as text unless a channel opts into Markdown-image media extraction, while preserving Telegram Markdown-image media replies and explicit <code>MEDIA:</code> attachments. Fixes #72642. Thanks @solavrc and @Bartok9.</li>
<li>Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.</li>
<li>Cron: apply <code>cron.maxConcurrentRuns</code> to the nested isolated-agent lane, start isolated execution timeouts only after the runner enters that lane, keep legacy flat <code>jobs.json</code> rows loadable, invalidate stale pending runtime slots after schedule edits, and preserve due slots for formatting-only rewrites. Fixes #72707, #27996, #71607, and #41783; carries forward #71651. Thanks @kagura-agent, @xialonglee, @fagnersouza666, @ayanesakura, and @Hurray0.</li>
<li>Cron/delivery: classify isolated successes, quiet <code>NO_REPLY</code> turns, model/provider failures, execution denials, <code>--no-deliver</code> traces, skipped-job alerts, and verified delivery outcomes correctly so cron history, retries, and failure counters reflect what actually happened. Fixes #72732, #50170, #43604, #68452, #60846, #72210, and #67172; follow-up to #54188; carries forward #43631, #68453, #72219, and #67186. Thanks @zNatix, @pixeldyn, @ChickenEggRoll, @SPFAdvisors, @anyech, @slideshow-dingo, @hatemclawbot-collab, @xydigit-sj, @oc-gh-dr, @hclsys, and @1yihui.</li>
<li>Cron/routing: preserve direct Telegram thread/account IDs, explicit Discord <code>user:</code>/<code>channel:</code> delivery targets, and <code>session:<id></code> failure-destination routing so reminders, cron announcements, and failure alerts keep the intended recipient kind across direct and group chats. Fixes #44270; refs #62777; carries forward #44325, #44351, #44412, #72657, #68535, and #62798. Thanks @RunMintOn, @arkyu2077, @0xsline, @vincentkoc, @slideshow-dingo, @likewen-tech, and @neeravmakwana.</li>
<li>Subagents: keep the delegated task only in the subagent system prompt and send a short initial kickoff message, avoiding duplicate task tokens while preserving multiline task formatting. Fixes #72019; carries forward #72053. Thanks @Wizongod and @ly85206559.</li>
<li>Onboarding/GitHub Copilot: add manifest-owned <code>--github-copilot-token</code> support for non-interactive setup, including env fallback, tokenRef storage in ref mode, saved-profile reuse, and current Copilot default-model wiring. Refs #50002 and supersedes #50003. Thanks @scottgl9.</li>
<li>Gateway/install: add a validated <code>--wrapper</code>/<code>OPENCLAW_WRAPPER</code> service install path that persists executable LaunchAgent/systemd wrappers across forced reinstalls, updates, and doctor repairs instead of falling back to raw node/bun <code>ProgramArguments</code>. Fixes #69400. (#72445) Thanks @willtmc.</li>
<li>Plugins: fail plugin registration when loader-owned acceptance gates reject missing hook names or memory-only capability registration from non-memory plugins, surfacing the issue through plugin status and doctor instead of silently dropping the registration. Fixes #72459. Thanks @amknight.</li>
<li>macOS Gateway: write launchd services with a state-dir <code>WorkingDirectory</code>, use a durable state-dir temp path instead of freezing macOS session <code>TMPDIR</code>, create that temp directory before bootstrap, and label abort-shaped launchd exits as <code>SIGABRT/abort</code> in status output. Fixes #53679 and #70223; refs #71848. Thanks @dlturock, @stammi922, and @palladius.</li>
<li>Control UI/update: make <code>Update now</code> require a real gateway process replacement, report skipped/error update outcomes with stable reasons, and verify the running gateway version after restart so global installs cannot silently keep old code in memory. Fixes #62492; addresses #64892 and #63562. Thanks @IAMSamuelRodda.</li>
<li>Exec approvals: accept runtime-owned <code>source: "allow-always"</code> and <code>commandText</code> allowlist metadata in gateway and node approval-set payloads so Control UI round-trips no longer fail with <code>unexpected property 'source'</code>. Fixes #60000; carries forward #60064. Thanks @sd1471123, @sharkqwy, and @luoyanglang.</li>
<li>Exec/node: skip approval-plan preparation for full-trust <code>host=node</code> runs so interpreter and script commands no longer fail with <code>SYSTEM_RUN_DENIED: approval cannot safely bind</code> when effective policy is <code>security=full</code> and <code>ask=off</code>. Fixes #48457 and duplicate #69251. Thanks @ajtran303, @jaserNo1, @Blakeshannon, @lesliefag, and @AvIsBeastMC.</li>
<li>Exec/node: synthesize a local approval plan when a paired node advertises <code>system.run</code> without <code>system.run.prepare</code>, unblocking approval-required <code>host=node</code> exec on current macOS companion nodes while preserving remote prepare for node hosts that support it. Fixes #37591 and duplicate #66839; carries forward #69725. Thanks @soloclz.</li>
<li>Memory/QMD: prefer QMD's <code>--mask</code> collection pattern flag so root memory indexing stays scoped to <code>MEMORY.md</code> instead of widening to every markdown file in the workspace. Fixes #65480; supersedes #65481 and #66259. Thanks @ccage-simp, @Bortlesboat, @seank-com, and @crazyscience.</li>
<li>Memory/doctor: treat the specific <code>gateway timeout after ...</code> gateway memory probe result as inconclusive instead of reporting embeddings not ready, while preserving warnings for explicit failures. Fixes #44426; carries forward #46576 with the Greptile review feedback applied. Thanks Cengiz (@ghost).</li>
<li>Gateway/startup: defer QMD, core request handlers, setup wizard, CLI outbound senders, plugin HTTP routes, chat/session projection, node session runtime validation, embedded-run activity reads, MCP loopback server imports, channel runtime helpers, HTTP/canvas/plugin auth helpers, isolated cron imports, and hook dispatch parsing until their request or shutdown paths, while making plain <code>gateway status</code> use a parse-only config snapshot so no-plugin boots and status reads avoid broad runtime fanout. Thanks @vincentkoc.</li>
<li>Lobster/Gateway: memoize repeated Ajv schema compilation before loading the embedded Lobster runtime so scheduled workflows and <code>llm.invoke</code> loops stop growing gateway heap on content-identical schemas. Fixes #71148. Thanks @cmi525, @vsolaz, and @vincentkoc.</li>
<li>Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in <code>/status</code>, <code>session_status</code>, or persisted <code>sessionEntry.totalTokens</code>. Fixes #69298. Thanks @richardmqq.</li>
<li>Hooks/session-memory: use the host local timezone for memory filenames, fallback timestamp slugs, and markdown headers instead of UTC dates. Fixes #46703. (#46721) Thanks @Astro-Han.</li>
<li>Gateway health: preserve live runtime-backed channel/account state in <code>gateway.health</code> snapshots and cached refreshes while keeping raw probe payloads on sensitive/admin paths only. (#39921, #42586, #46527, #52770, #42543) Thanks @FAL1989, @rstar327, @0xble, and @ajayr.</li>
<li>Feishu: extract quoted/replied interactive-card text across schema 1.0, schema 2.0, i18n, template-variable, and post-format fallback shapes without carrying broad generated/config churn from related parser experiments. (#38776, #60383, #42218, #45936) Thanks @lishuaigit, @lskun, @just2gooo, and @Br1an67.</li>
<li>Telegram/agents: hide raw failed write/edit warning messages in Telegram when the assistant already explicitly acknowledges the failed action, while keeping warnings when the reply claims success or omits the failure; #39406 remains the broader configurable delivery-policy follow-up. Fixes #51065; covers #39631. Thanks @Bartok9 and @Bortlesboat.</li>
<li>Exec approvals: accept a symlinked <code>OPENCLAW_HOME</code> as the trusted approvals root while still rejecting symlinked <code>.openclaw</code> path components below it. (#64663) Thanks @FunJim.</li>
<li>Logging: add top-level <code>hostname</code>, flattened <code>message</code>, and available <code>agent_id</code>, <code>session_id</code>, and <code>channel</code> fields to file-log JSONL records for multi-agent filtering without removing existing structured log arguments. Fixes #51075. Thanks @stevengonsalvez.</li>
<li>ACP: route server logs to stderr before Gateway config/bootstrap work so ACP stdout remains JSON-RPC only for IDE integrations. Fixes #49060. Thanks @Hollychou924.</li>
<li>Logging: propagate internal request trace scopes through Gateway HTTP requests and WebSocket frames so file logs, diagnostic events, agent run traces, model-call traces, OTEL spans, and trusted provider <code>traceparent</code> headers share a correlatable <code>traceId</code> without logging raw request or model content. Fixes #40353. Thanks @liangruochong44-ui.</li>
<li>Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.</li>
<li>Logging: write validated diagnostic trace context as top-level <code>traceId</code>, <code>spanId</code>, <code>parentSpanId</code>, and <code>traceFlags</code> fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.</li>
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
<li>Agents/sessions: let <code>sessions_spawn runtime="subagent"</code> ignore ACP-only <code>streamTo</code> and <code>resumeSessionId</code> fields while keeping ACP passthrough and documenting <code>streamTo</code> as ACP-only. Fixes #43556 and #63120; covers #56326, #61724, #64714, and #67248; carries forward #68397, #65282, #58686, #56342, and #40102. Thanks @skernelx, @damselem, @Br1an67, @Mintalix, @IsaacAPerez, @vvitovec, @Sanjays2402, @shenkq97, and @1034378361.</li>
<li>Providers/Ollama: honor <code>/api/show</code> capabilities, custom Modelfile <code>PARAMETER num_ctx</code>, configured provider/model context defaults, whitelisted native params such as <code>temperature</code>, <code>top_p</code>, and <code>think</code>, and native thinking effort levels so local models get accurate tools, context, and thinking behavior without forcing full-context VRAM use. Fixes #64710, duplicate #65343, #68344, #44550, #52206, #49684, #68662, #48010, #71584, and #44786; supersedes #69464; carries forward #44955. Thanks @yuan-b, @netherby, @xilopaint, @Diyforfun2026, @neeravmakwana, @taitruong, @armi0024, @LokiCode404, @zhouZcong, @dshenster-byte, @tangzhi, @pandego, @maweibin, @Adam-Researchh, @EmpireCreator, @g0st1n, and @voltwake.</li>
<li>Image tool/media: honor <code>tools.media.image.timeoutSeconds</code> and matching per-model image timeouts in explicit image analysis, including the MiniMax VLM fallback path, so slow local vision models are not capped by hardcoded 30s/60s aborts. Fixes #67889; supersedes #67929. Thanks @AllenT22 and @alchip.</li>
<li>Providers/Ollama: strip custom provider prefixes before native chat/embedding requests, skip ambient localhost discovery unless config/auth opts in, handle custom remote <code>api: "ollama"</code> providers, accept OpenAI SDK-style <code>baseURL</code>, scope synthetic local auth and embedding bearer headers to declared host boundaries, resolve custom-named local providers for subagents, add provider-scoped model request timeouts, preserve explicit input modalities, and document <code>params.keep_alive</code> plus local/LAN/cloud/multi-host/web-search/embedding/thinking setup recipes. Fixes #72353, #56939, #62533, #43945, #64541, #68796, and #39690; supersedes #57116, #62549, #69261, #69857, #65143, and #66511; refs #43945; carries forward #43224 and #39785. Thanks @maximus-dss, @hclsys, @IanxDev, @tsukhani, @issacthekaylon, @Julien-BKK, @Linux2010, @hyspacex, @maxramsay, @Meli73, @LittleJakub, @Juankcba, @uninhibite-scholar, @yfge, @Skrblik, and @Mriris.</li>
<li>Providers/Ollama: move memory embeddings to <code>/api/embed</code> with batched <code>input</code>, route local web search through Ollama's signed daemon proxy while keeping cloud auth scoped, treat Ollama memory embeddings as key-optional in doctor, and keep model usage visible by estimating native transcript usage when <code>/api/chat</code> omits counters. Fixes #39983, #69132, and #46584; carries forward #39112. Thanks @sskkcc, @LiudengZhang, @yoon1012, @hyspacex, @fengly78, and @TylonHH.</li>
<li>Agents/Ollama: parse stringified native tool-call arguments, retry native empty/thinking-only turns, accept already-prefixed LLM task model overrides, apply provider-owned replay normalization for Cloud models, validate explicit <code>--thinking max</code>, show resolved thinking defaults in Control UI, and include configured provider models in <code>models list --provider</code>. Fixes #69735, #50052, #71697, #71584, #72407, and #65207; supersedes #69910; carries forward #66552 and #61223. Thanks @rongshuzhao, @yfge, @L3G, @ralphy-maplebots, @Hollychou924, @ismael-81, @g0st1n, @NotecAG, and @drzeast-png.</li>
<li>Providers/PDF/Ollama: add bounded network timeouts for Ollama model pulls and native Anthropic/Gemini PDF analysis requests so unresponsive provider endpoints no longer hang sessions indefinitely. Fixes #54142; supersedes #54144 and #54145. Thanks @jinduwang1001-max and @arkyu2077.</li>
<li>Docker/QA: add observability coverage to the normal Docker aggregate so QA-lab OTEL and Prometheus diagnostics run inside Docker. Thanks @vincentkoc.</li>
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
<li>Agents/model fallback: keep auto-persisted fallback model overrides selected across turns until <code>/new</code> or reset clears them, avoiding repeated probes of a known-bad primary while <code>/status</code> shows the selected and active models. Thanks @kibedu.</li>
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
<li>Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways.</li>
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>.</li>
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
<li>Plugins/compat/CLI: inventory doctor-side deprecation migrations separately from runtime plugin compatibility, add dated records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims, refresh the persisted registry after managed plugin removals, make plugin install/uninstall writes conflict-aware, clear stale denylists, and fail tracked plugin/hook updates or unloadable package installs instead of leaving stale state. Thanks @vincentkoc.</li>
<li>WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.</li>
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
<li>Gateway/session rows: report the same config-resolved thinking default that runtime sessions use, including global and per-agent defaults, so Control UI and TUI default labels stay aligned. (#71779, #70981, #71033, #70302) Thanks @chen-zhang-cs-code, @SymbolStar, and @cholaolu-boop.</li>
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans.</li>
<li>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.</li>
<li>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.</li>
<li>Cron/context engine: run isolated cron jobs under run-scoped context-engine session keys so prior runs of the same job are not inherited unless the job is explicitly session-bound. (#72292) Thanks @jalehman.</li>
<li>Control UI: localize command palette labels, categories, skill shortcuts, footer hints, and connect-command copy labels while preserving localized command palette search matching. (#61130, #61119) Thanks @rubensfox20.</li>
<li>Plugins/memory-lancedb: request float embedding responses from OpenAI-compatible servers so local providers that default SDK requests to base64 no longer return dimension-mismatched LanceDB vectors while preserving configured dimensions. Fixes #45982. (#59048, #46069, #45986) Thanks @deep-introspection, @xiaokhkh, @caicongyang, and @thiswind.</li>
<li>Plugins/memory-lancedb: advance auto-capture cursors per session only after messages are processed or intentionally skipped, retry failed messages, survive compacted histories, and clear cursor state on session end. Fixes #71349; carries forward #42083. Thanks @as775116191.</li>
<li>Plugins/memory-core: respect configured memory-search embedding concurrency during non-batch indexing so local Ollama embedding backends can serialize indexing instead of flooding the server. Fixes #66822. (#66931) Thanks @oliviareid-svg and @LyraInTheFlesh.</li>
<li>Docker/update smoke: keep the package-derived update-channel fixture on package-shipped files and make its UI build stub create the asset the updater verifies. Thanks @vincentkoc.</li>
<li>Gateway/models: repair legacy <code>models.providers.*.api = "openai"</code> config values to <code>openai-completions</code>, and skip providers with future stale API enum values during startup instead of bricking the gateway. Fixes #72477. (#72542) Thanks @JooyoungChoi14 and @obviyus.</li>
<li>Gateway/skills: redact <code>apiKey</code> and secret-named <code>env</code> values from the <code>skills.update</code> RPC response to prevent leaking credentials into WebSocket traffic, client logs, or session transcripts. Config is still written to disk in full; only the response payload is redacted. (#69998) Thanks @Ziy1-Tan.</li>
<li>Plugins/CLI: let flag-driven <code>openclaw channels add</code> install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation.</li>
<li>Onboarding/setup: keep first-run config reads, plugin compatibility notices, OpenAI Codex auth, post-auth default-model policy lookup, skip-auth, provider-scoped model pickers, and post-model sanity checks on cold manifest/setup metadata unless the user chooses to browse all models, avoiding full plugin/provider runtime loads between prompts. Thanks @shakkernerd.</li>
<li>Gateway/Bonjour: suppress known @homebridge/ciao cancellation and network assertion failures through scoped process handlers so malformed mDNS packets or restricted VPS networking disable/restart Bonjour instead of crashing the gateway. Fixes #67578. Thanks @zenassist26-create.</li>
<li>Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse.</li>
<li>Telegram: send a fresh final message for long-lived preview-streamed replies so the visible Telegram timestamp reflects completion time instead of the preview creation time. Thanks @rubencu.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.26/OpenClaw-2026.4.26.zip" length="48222029" type="application/octet-stream" sparkle:edSignature="6wgFZUyyU09Y6nvD9T1Ufq7Plo0Wzfg+L9r80DCaNMMuwebcKWAsMVSP3RvhRhTxVMax8toUDYg3gb/vOiE5BA=="/>
</item>
<item>
<title>2026.4.25</title>
<pubDate>Mon, 27 Apr 2026 13:34:25 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042590</sparkle:version>
<sparkle:shortVersionString>2026.4.25</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.25</h2>
<h3>Highlights</h3>
<ul>
<li>Voice replies get a full TTS upgrade: <code>/tts latest</code>, chat-scoped auto-TTS controls, personas, per-agent/per-account overrides, and new Azure Speech, Xiaomi, Local CLI, Inworld, Volcengine, and ElevenLabs v3 provider coverage. Thanks @leonchui, @zoujiejun, @solar2ain, @cshape, @xuruiray, @itsuzef, and @barronlroth.</li>
<li>Plugin startup and install paths move to the cold persisted registry, cutting broad manifest scans while making plugin update, repair, provider discovery, and install metadata more deterministic. Thanks @vincentkoc and @shakkernerd.</li>
<li>OpenTelemetry coverage expands across model calls, token usage, tool loops, harness runs, exec processes, outbound delivery, context assembly, and memory pressure with bounded low-cardinality attributes. Thanks @vincentkoc, @jlapenna, @Lidang-Jiang, and @oc-factus.</li>
<li>Browser automation gets safer tab URLs, iframe-aware role snapshots, CDP readiness tuning, headless one-shot launch, and deeper browser doctor probes for slow hosts. Thanks @beat843796 and @BenediktSchackenberg.</li>
<li>Control UI and setup flows add PWA/Web Push support, Crestodian first-run repair, TUI setup, context mode selection, and a shorter startup greeting. Thanks @eduardocruz, @SebTardif, and @kevinlin-openai.</li>
<li>Install/update hardening covers Windows, macOS, Linux, Docker, bundled plugin runtime deps, Node service restarts, LaunchAgent token rotation, and mixed-version gateway verification. Thanks @Kobevictor, @igormf, @abhinas90, @jsompis, @Solvely-Colin, and @gucasbrg.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>TTS/WhatsApp: add <code>/tts latest</code> read-aloud support with duplicate suppression and <code>/tts chat on|off|default</code> session-scoped auto-TTS overrides, completing the on-demand voice-note UX for current-chat replies. Fixes #66032.</li>
<li>TTS/channels: resolve channel and account TTS overrides generically, enabling Feishu and QQBot accounts to deep-merge <code>channels.<channel>.accounts.<id>.tts</code> over global and per-agent TTS config. Thanks @sahilsatralkar.</li>
<li>TTS/agents: allow <code>agents.list[].tts</code> to override global <code>messages.tts</code> for per-agent voices, and make <code>/tts audio</code>, <code>/tts status</code>, and the <code>tts</code> agent tool honor the active voice/provider override while keeping shared provider credentials and preferences in the existing TTS config surface.</li>
<li>Providers/Azure Speech: add Azure Speech as a bundled TTS provider with Speech-resource auth, voice listing, SSML escaping, native Ogg/Opus voice-note output, and telephony output. (#51776) Thanks @leonchui.</li>
<li>Google Meet: add calendar-backed attendance export workflows, export manifests, dry-run previews, and tool parity for meeting records.</li>
<li>Control UI: add PWA install support and Web Push notifications for Gateway chat. (#44590) Thanks @eduardocruz.</li>
<li>Browser automation: add safe tab URLs in agent responses plus a CDP-native role snapshot fallback with iframe-aware refs, cursor-clickable detection, target attach preparation, and <code>openclaw browser doctor --deep</code> live snapshot probing.</li>
<li>CLI/image generation: expose generic <code>--background</code> on <code>openclaw infer image generate</code> and <code>openclaw infer image edit</code>, keep <code>--openai-background</code> as an OpenAI alias, and let fal image generation honor <code>--output-format png|jpeg</code>.</li>
<li>Browser/config: allow local managed Chrome launch discovery and post-launch CDP readiness timeouts to be raised for slower hosts such as Raspberry Pi. Fixes #66803. Thanks @beat843796.</li>
<li>Discord: allow <code>channels.discord.voice.model</code> to override the LLM used for voice channel responses while keeping STT and TTS on their existing media settings. (#64368) Thanks @mrdavey.</li>
<li>Browser/CLI: add <code>openclaw browser start --headless</code> as a one-shot local managed browser launch override without rewriting persisted browser config. Thanks @BenediktSchackenberg.</li>
<li>CLI/Crestodian/TUI: add the first-run setup helper, local planner fallback, full-TUI interactive Crestodian, startup progress indicators, context mode selector, and a shorter startup greeting. (#71720, #71760) Thanks @SebTardif and @kevinlin-openai.</li>
<li>Plugins: migrate the local plugin registry automatically during package install/update, keeping install metadata in the plugin index while indexing existing plugin manifests for the new cold registry path. Thanks @vincentkoc and @shakkernerd.</li>
<li>Plugins/doctor: make <code>openclaw doctor --fix</code> refresh the plugin index and cold registry index when needed without treating plugin install records as authored config. Thanks @vincentkoc and @shakkernerd.</li>
<li>Plugins/hooks: add before-agent-finalize hooks, cron <code>jobId</code> hook context, bounded native permission fingerprints, and Codex MCP hook relay support. (#71765, #71758, #71707) Thanks @vincentkoc and @pashpashpash.</li>
<li>Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.6.3. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy <code>gen_ai.system</code> by default while emitting <code>gen_ai.provider.name</code> under <code>OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental</code>. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: support signal-specific OTLP endpoint overrides for traces, metrics, and logs via config or standard OTEL environment variables. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: emit bounded telemetry exporter health diagnostics for startup and log-export failures without exporting raw error text. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: export agent harness lifecycle telemetry as bounded <code>openclaw.harness.run</code> spans and <code>openclaw.harness.duration_ms</code> metrics so QA-lab, Codex, and future harnesses share one trace shape. Thanks @vincentkoc.</li>
<li>Diagnostics/trace: propagate W3C <code>traceparent</code> headers from trusted model-call trace context to provider transports while replacing caller-supplied traceparent values. Thanks @vincentkoc.</li>
<li>Diagnostics/Prometheus: add a bundled <code>diagnostics-prometheus</code> plugin with a protected gateway scrape route for low-cardinality diagnostics metrics. Thanks @vincentkoc.</li>
<li>Plugins/CLI: add <code>openclaw plugins registry</code> for explicit persisted-registry inspection and <code>--refresh</code> repair without making normal startup rescan plugin locations. Thanks @vincentkoc.</li>
<li>Plugins/CLI: make <code>openclaw plugins list</code> read the cold persisted registry snapshot by default, leaving module-aware diagnostics to <code>plugins doctor</code> and <code>plugins inspect</code>. Thanks @vincentkoc.</li>
<li>Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.</li>
<li>Plugins/startup: normalize startup and provider plugin enablement through registry aliases so boot paths do not need the legacy manifest alias scan. Thanks @vincentkoc.</li>
<li>Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc.</li>
<li>Plugins/registry: keep installed plugin index records focused on install/state/load paths and resolve plugin capabilities from manifests scoped to indexed plugins. Thanks @shakkernerd.</li>
<li>Plugins/registry: route cold manifest and capability lookups through the installed plugin index so setup, channels, config, secrets, doctor, and provider metadata paths avoid broad plugin-root scans before runtime execution. Thanks @shakkernerd.</li>
<li>CLI/models: speed up <code>models list --all --provider <id></code> for static manifest-backed providers by loading catalog rows through the installed plugin index instead of broad manifest scans or runtime suppression hooks. Thanks @shakkernerd.</li>
<li>CLI/models: use OpenClaw Provider Index preview rows as the final cold fallback for installable providers, while keeping user config, installed manifests, and refreshed cache rows above provider-index metadata. Thanks @vincentkoc.</li>
<li>Providers/plugins: keep onboarding and auth-choice setup lists on cold manifest/install metadata and add Provider Index install metadata for not-yet-installed provider plugins. Thanks @vincentkoc.</li>
<li>Providers/plugins: keep provider setup guidance and configure auth imports on cold manifest metadata, with a regression guard against static provider-runtime imports on setup/configure list paths. Thanks @vincentkoc.</li>
<li>CLI/capabilities: keep capability command registration from importing the models auth runtime until <code>model auth login</code> actually runs. Thanks @vincentkoc.</li>
<li>CLI/configure: keep web-search configure prompts on cold plugin registry metadata until the user chooses managed search setup. Thanks @vincentkoc.</li>
<li>Plugins/chat commands: refresh the persisted plugin registry after <code>/plugins enable</code> and <code>/plugins disable</code>, matching the CLI mutation path. Thanks @vincentkoc.</li>
<li>Plugins/compat: mark <code>OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY</code> as a deprecated break-glass switch and point operators at registry repair instead. Thanks @vincentkoc.</li>
<li>Plugins/compat: expand the central compatibility registry with dated owners, replacements, and maximum three-month removal targets for legacy SDK, manifest, setup, registry-migration, and agent-runtime surfaces. Thanks @vincentkoc.</li>
<li>Plugins/registry: ignore stale persisted registry reads when plugin policy no longer matches current config, and stamp generated registry files with a do-not-edit warning. Thanks @vincentkoc.</li>
<li>Config/plugins: keep plugin command-alias validation on cold manifest metadata instead of importing the runtime alias resolver. Thanks @vincentkoc.</li>
<li>Security/plugins: keep web-search credential presence checks on cold config, env, and manifest metadata instead of importing web-search provider runtime. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: surface provider request identifiers as bounded hashes on model-call diagnostics and span events, without exporting raw request IDs or metric labels. Thanks @Lidang-Jiang and @vincentkoc.</li>
<li>Plugins/diagnostics: add metadata-only <code>model_call_started</code> and <code>model_call_ended</code> hooks for provider/model call telemetry without exposing prompts, responses, headers, request bodies, or raw provider request IDs. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: emit bounded context assembly diagnostics and export <code>openclaw.context.assembled</code> spans with prompt/history sizes but no prompt, history, response, or session-key content. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: export existing tool-loop diagnostics as <code>openclaw.tool.loop</code> counters and spans without loop messages, session identifiers, params, or tool output. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: export diagnostic memory samples and pressure as bounded memory histograms, counters, and pressure spans to help spot leak regressions without session or payload data. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: add the GenAI <code>gen_ai.client.token.usage</code> histogram for input/output model usage while keeping session identifiers and aggregate cache counters out of the semantic metric. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: add a bounded <code>openclaw.agent</code> label to OpenClaw token metrics so per-agent Grafana dashboards can group usage without exporting session identifiers. Thanks @oc-factus.</li>
<li>Plugins/install: consolidate managed plugin install metadata into the state-managed plugin index at <code>plugins/installs.json</code>, replacing the temporary <code>plugins/installed-index.json</code> path and removing <code>plugins.installs</code> as an authored config surface. Thanks @vincentkoc and @shakkernerd.</li>
<li>Diagnostics/OTEL: add the GenAI <code>gen_ai.client.operation.duration</code> histogram for model-call latency in seconds with bounded provider/model/API and error attributes. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: add GenAI usage token attributes to model-usage spans, including cache read/write input token counts without session identifiers or prompt/response content. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: include bounded GenAI operation, provider, and request-model attributes on model-usage spans so token usage remains self-describing without diagnostic identifiers. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: keep model-usage span GenAI provider attributes aligned with the existing semantic-convention opt-in policy, using legacy <code>gen_ai.system</code> unless latest experimental GenAI conventions are enabled. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: keep <code>gen_ai.request.model</code> present on GenAI token usage metrics with a bounded <code>unknown</code> fallback when model usage events do not include a model. Thanks @vincentkoc.</li>
<li>Docs/OTEL: document the GenAI token and model-call duration metrics, model-usage span attributes, and <code>OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental</code> provider-attribute behavior. Thanks @vincentkoc.</li>
<li>Docs: refresh the MCP, model provider, doctor, troubleshooting, BlueBubbles, media generation, TTS, subagents, skills, cron/tasks, exec approvals, and voice-call guides with structured Steps, Tabs, and Accordion content.</li>
<li>Diagnostics/trace: add an internal traceparent propagation helper that only formats trusted dispatcher metadata, keeping plugin-emitted diagnostic traces out of outbound propagation by default. Thanks @vincentkoc.</li>
<li>Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.</li>
<li>Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as <code>openclaw.exec</code> spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.</li>
<li>Diagnostics/OTEL: support <code>OPENCLAW_OTEL_PRELOADED=1</code> so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.</li>
<li>Providers/Xiaomi: add MiMo TTS as a bundled speech provider with MP3/WAV output and voice-note Opus transcoding. Fixes #52376. (#55614) Thanks @zoujiejun.</li>
<li>Providers/ElevenLabs: include <code>eleven_v3</code> in the bundled TTS model catalog so model selection surfaces can offer ElevenLabs v3. (#68321) Thanks @itsuzef.</li>
<li>Providers/Local CLI TTS: add a bundled local command speech provider with file/stdout input, voice-note Opus conversion, and telephony PCM output. (#56239) Thanks @solar2ain.</li>
<li>Providers/Inworld: add Inworld as a bundled speech provider with streaming TTS synthesis, voice listing, voice-note output, and PCM telephony output. (#55972) Thanks @cshape.</li>
<li>Providers/Volcengine: add Volcengine/BytePlus Seed Speech as a bundled TTS provider with API-key auth, native Ogg/Opus voice-note output, and MP3 audio-file output. (#55641) Thanks @xuruiray.</li>
<li>Android/Talk Mode: expose Talk Mode in the Voice tab with runtime-owned voice capture modes and microphone foreground-service escalation. Thanks @alex-latitude.</li>
<li>Providers/LiteLLM: register <code>litellm</code> as an image-generation provider so <code>image_generate model=litellm/...</code> calls and <code>agents.defaults.imageGenerationModel.fallbacks</code> entries resolve through the LiteLLM proxy. Thanks @zqchris.</li>
<li>Providers/fal: add Seedance 2.0 reference-to-video models with multi-image, video, and audio reference input mapping plus model-specific capability limits for <code>video_generate</code>. Thanks @shivanker.</li>
<li>Codex harness: require Codex app-server <code>0.125.0</code> or newer and cover native MCP <code>PreToolUse</code>, <code>PostToolUse</code>, and <code>PermissionRequest</code> payloads through the OpenClaw hook relay.</li>
<li>Agents/Codex: teach prompts and <code>agents_list</code> to surface native Codex app-server availability so agents prefer <code>/codex ...</code> over Codex ACP unless ACP/acpx is explicit. Thanks @vincentkoc.</li>
<li>ACPX/Droid: add Factory Droid to the live ACP bind Docker matrix, including <code>.factory</code> settings staging, <code>FACTORY_API_KEY</code> forwarding, and the single-agent <code>test:docker:live-acp-bind:droid</code> recipe.</li>
<li>TTS/personas: add provider-aware TTS personas with deterministic provider binding merges, <code>/tts persona</code> controls, gateway/CLI persona state, Google Gemini <code>audio-profile-v1</code> prompt wrapping, and OpenAI instruction mapping. (#70748) Thanks @barronlroth.</li>
<li>Voice Wake: add trigger-based routing so macOS voice wake phrases can select a configured agent or session target, with Gateway routing APIs and node update events. (#30354) Thanks @longbiaochen.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
<li>Agents/OpenAI: keep Responses web search compatible with minimal thinking by raising <code>web_search</code> requests to the lowest supported reasoning effort instead of sending a rejected minimal payload.</li>
<li>Agents/tools: honor the <code>bundle-mcp</code> allowlist token when deciding whether bundled MCP tools are available, so restricted tool policies can still enable bundled MCP without exposing unrelated tools.</li>
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
<li>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.</li>
<li>Plugins/CLI: let flag-driven <code>openclaw channels add</code> install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation.</li>
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>.</li>
<li>Plugins/install: materialize plugin-owned root chunks in external bundled-runtime mirrors so staged plugin dependencies resolve under native ESM in packaged installs. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss.</li>
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
<li>Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.</li>
<li>Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.</li>
<li>Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale <code>plugins list</code> entries.</li>
<li>Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds.</li>
<li>Plugins: fail <code>plugins update</code> when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries.</li>
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans.</li>
<li>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.</li>
<li>Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd.</li>
<li>Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd.</li>
<li>Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd.</li>
<li>Onboarding/models: keep skip-auth and provider-scoped model picker prompts off the full global model catalog path, and cache provider catalog hook resolution so setup no longer stalls after auth on large plugin registries. Thanks @shakkernerd.</li>
<li>Gateway/Bonjour: suppress known @homebridge/ciao cancellation and network assertion failures through scoped process handlers so malformed mDNS packets or restricted VPS networking disable/restart Bonjour instead of crashing the gateway. Fixes #67578. Thanks @zenassist26-create.</li>
<li>Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse.</li>
<li>Agents/subagents: deliver completed yielded-subagent results back to no-thread requester routes via direct fallback when the dormant parent announce turn produces no visible reply, and add QA-lab coverage for the regression. Thanks @vincentkoc.</li>
<li>Gateway/Tailscale: let Tailscale-authenticated Control UI operator sessions with browser device identity skip the device-pairing round trip while still rejecting device-less and node-role connections. Refs #71986. Thanks @jokedul.</li>
<li>Doctor: honor <code>OPENCLAW_SERVICE_REPAIR_POLICY=external</code> by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd.</li>
<li>CLI/update: run package post-update doctor with <code>--fix</code> so package updates repair config migrations before restart. Thanks @shakkernerd.</li>
<li>CLI/update: retry failed npm global updates with <code>--omit=optional</code> and ignore the superseded first failure when the fallback succeeds. Thanks @shakkernerd.</li>
<li>Plugins/uninstall: migrate and reset <code>plugins.slots.contextEngine</code> alongside memory slots when plugin ids change or selected plugins are removed. Thanks @shakkernerd.</li>
<li>Agents/Discord: keep raw <code>Agent failed before reply</code> runner failures out of Discord group/channel chats and show detailed runner errors in direct chats only when <code>/verbose</code> is enabled.</li>
<li>UI/Windows: quote resolved pnpm <code>.cmd</code> launcher paths before spawning UI install/build/test commands so Node installs under <code>C:\Program Files</code> no longer fail as <code>C:\Program</code>. Fixes #45275. Thanks @Kobevictor, @stoppieboy, and @iubns.</li>
<li>Codex/agent: translate <code>--thinking minimal</code> to <code>low</code> for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive <code>minimal</code> directly. Fixes #71946. Thanks @hclsys.</li>
<li>Plugins/uninstall: remove tracked plugin files from their recorded managed extensions root even when the current state directory points somewhere else, so <code>openclaw plugins uninstall --force</code> does not leave the plugin discoverable. Thanks @shakkernerd.</li>
<li>Agents/runtime: add <code>agentRuntime.id</code> as the canonical config key, migrate legacy runtime-policy configs with <code>openclaw doctor --fix</code>, route canonical Anthropic models through <code>claude-cli</code> without passing CLI backend aliases to embedded harness selection, and load CLI backend owner plugins before channel startup. Fixes #71957. Thanks @WolvenRA.</li>
<li>CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on <code>schtasks /End</code> before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang.</li>
<li>Windows install/Lobster: execute <code>pnpm.exe</code> directly when <code>npm_execpath</code> points at the native pnpm binary, add an installed-package fallback for the Lobster embedded runtime, and include the Lobster runner regression test in Windows CI. Fixes #69456. Thanks @igormf.</li>
<li>Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.</li>
<li>Update: ignore bundled plugin <code>.openclaw-install-stage</code> directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into <code>unexpected packaged dist file</code> failures. Fixes #71752. Thanks @waynegault.</li>
<li>CLI/update: fail package updates when post-update plugin sync fails and refresh legacy npm plugin install records before trusting unchanged artifacts, preventing successful updates from restarting with stale or failed plugin state. Thanks @vincentkoc and @shakkernerd.</li>
<li>Release/update: reject pre-populated bundled plugin <code>.openclaw-install-stage</code> directories, including mixed-case path variants, before package inventory generation so release tarballs cannot ship poisoned runtime-dependency staging debris. Fixes #71752. Thanks @hclsys.</li>
<li>Node runtime: keep node-host retry timers alive across Gateway restarts and exit on terminal credential pauses so supervised nodes do not become silent zombies. Fixes #69800. Thanks @meroli28.</li>
<li>Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when <code>channels.whatsapp</code> is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008.</li>
<li>Gateway/device tokens: enforce caller-scope containment inside token rotation and revocation so pairing-only sessions cannot mutate higher-scope operator tokens. Fixes #71990. Thanks @coygeek.</li>
<li>Plugins/channels: keep security checks, thread-binding placement, provider summaries, health formatting, and message action labels on read-only or already-loaded channel metadata instead of importing full channel runtime. Thanks @shakkernerd.</li>
<li>Plugins/status: keep config-only channel labels and status security summaries from importing plugin runtime modules just to render metadata. Thanks @shakkernerd.</li>
<li>Sessions/channels: stop group-session metadata from loading bundled channel runtime just to classify <code>#channel</code> subjects, using only already-loaded channel capabilities on that path. Thanks @shakkernerd.</li>
<li>Plugins/channels: keep native command and native skill <code>auto</code> defaults on static channel metadata so config, audit, and command-list checks do not load channel runtime just to read those defaults. Thanks @shakkernerd.</li>
<li>CLI/channels: keep channel remove selection and all-channel capabilities summaries on read-only plugin metadata, loading channel runtime only for the selected mutation path. Thanks @shakkernerd.</li>
<li>CLI/models: keep Provider Index preview rows out of <code>models list --all --provider <id></code> when the owning provider plugin is disabled, preserving config authority for cold catalog fallbacks. Thanks @shakkernerd.</li>
<li>CLI/model runs: keep <code>openclaw infer model run</code> on explicit OpenRouter models from loading the full provider catalog or inheriting chat-agent silent-reply policy, restoring non-empty one-shot probe output. Fixes #68791. Thanks @limpredator.</li>
<li>Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming <code>node@24</code> was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.</li>
<li>Installer: load nvm before Node.js detection so <code>curl | bash</code> installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.</li>
<li>Installer/Windows: route PowerShell install failures through a top-level handler so <code>iwr ... | iex</code> returns control to the current shell while direct script-file runs still exit non-zero. Fixes #38054. Thanks @PwrSrg.</li>
<li>CLI/Volta: respawn raw <code>openclaw</code> CLI runs through the named <code>node</code> shim when the current Node executable resolves to <code>volta-shim</code>, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86.</li>
<li>Installer: warn when multiple npm global roots contain OpenClaw installs, showing active Node/npm/openclaw plus each install path and version so stale version-manager installs are visible. Fixes #40839. Thanks @zhixianio.</li>
<li>Cron/tasks: recover completed cron task ledger records from durable run logs and job state before marking them <code>lost</code>, reducing false <code>backing session missing</code> audit errors for isolated cron runs and keeping offline CLI audit from treating its empty local cron active-job set as authoritative. Fixes #71963.</li>
<li>Docker: copy patched dependency files into runtime images so downstream <code>pnpm install</code> layers keep working. Fixes #69224. Thanks @gucasbrg.</li>
<li>Package: include patched dependency files in the published npm package so downstream installs can resolve <code>patchedDependencies</code>. (#69224) Thanks @gucasbrg and @vincentkoc.</li>
<li>Plugins/channels: treat malformed bundled channel plugin loaders that return <code>undefined</code> as unavailable instead of crashing config and help paths. Fixes #69044. Thanks @frankhli843 and @vincentkoc.</li>
<li>Scripts/watch: show corrupted dependency package-config recovery guidance when <code>gateway:watch</code> fails during watcher startup, without double-logging unrelated import failures. (#58780) Thanks @roytong9 and @vincentkoc.</li>
<li>Signal: read signal-cli RPC, health checks, and SSE events through Node's HTTP client so Node 24/25 fetch regressions do not break Signal sends or inbound events. Fixes #51716 and #53040. Thanks @Barukimang, @minupla, and @vincentkoc.</li>
<li>Skills/Docker: run npm-backed skill dependency installs with an OpenClaw-managed user prefix so non-root Docker images do not write to <code>/usr/local</code>. Fixes #59601. Thanks @chanjarster and @vincentkoc.</li>
<li>Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker.</li>
<li>Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai.</li>
<li>Node/Linux: make <code>openclaw node install</code> enable and restart the <code>openclaw-node</code> systemd unit instead of the gateway unit on node-only VMs. Fixes #68287. Thanks @dlebee-agent.</li>
<li>Browser/CDP: retry transient raw-CDP WebSocket handshake failures before any browser command is sent, and reconnect stale persistent Playwright CDP sessions for safe tab-list reads without replaying mutating browser actions. Fixes #67728.</li>
<li>Gateway/Linux: retry <code>systemctl --user enable</code> after a second daemon reload when the freshly written gateway unit is not visible yet on migrated systemd installs. Fixes #65184. Thanks @liushuaiiu.</li>
<li>Telegram: preserve exact selected quote text when sending native quote replies, and retry with legacy replies if Telegram rejects quote parameters. (#71952) Thanks @rubencu.</li>
<li>Plugins/CLI: preserve manifest name, description, format, and source metadata in cold <code>openclaw plugins list</code> output without importing plugin runtime. Thanks @shakkernerd.</li>
<li>Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.</li>
<li>Plugins/chat: keep <code>/plugins list</code>, <code>/plugins enable</code>, and <code>/plugins disable</code> on the persisted plugin index path so chat plugin management does not load diagnostic/runtime plugin registries before execution. Thanks @shakkernerd.</li>
<li>Plugins/doctor: read workspace plugin status and legacy web-search ownership through installed-index manifest metadata instead of broad manifest registry scans. Thanks @shakkernerd.</li>
<li>CLI/agents: read channel provider status from read-only plugin index metadata for text <code>agents list</code> output instead of the loaded channel registry. Thanks @shakkernerd.</li>
<li>Logging: redact configured secret patterns at console and file-log sink exits so credentials that reach the logger are masked before terminal display or JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.</li>
<li>Gateway/services: refuse process and service mutations from an older OpenClaw binary when the config was last written by a newer version, preventing split-brain installs from stopping or rewriting newer gateway services. Fixes #57079.</li>
<li>Gateway: reserve <code>/healthz</code> and <code>/readyz</code> ahead of plugin, canvas, and Control UI HTTP stages so liveness/readiness probes still answer when a later route handler stalls. Fixes #69674. Thanks @Xike-Creek.</li>
<li>Logging: load <code>logging.file</code> and redaction settings directly from the active OpenClaw config path in bundled runtimes, so packaged gateways stop falling back to <code>/tmp/openclaw</code>. Fixes #59370, #67168, and #61295. Thanks @KeaneYan, @Pan9hu, and @zsjlovelike.</li>
<li>Logging: rotate file logs at <code>logging.maxFileBytes</code>, keep bounded numbered archives, and make long-lived rolling loggers follow the current-day file instead of suppressing diagnostics or writing stale dated files. Fixes #58583 and #62381. Thanks @jpeghead and @zhaoleink.</li>
<li>Agents/groups: treat clean empty assistant stops as silent <code>NO_REPLY</code> only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.</li>
<li>macOS/Node: keep native remote app nodes from advertising <code>browser.proxy</code>, start browser-capable CLI node services through the restored <code>openclaw node start</code> command, and show an actionable browser-control error when the local control service is missing. Fixes #66637.</li>
<li>Gateway/update: fail package updates when the restarted managed gateway reports the wrong version, including fallback restarts and JSON mode, avoiding false-success mixed-version restarts after macOS LaunchAgent updates. Fixes #71835. Thanks @abhinas90 and @jsompis.</li>
<li>Gateway/update: warn before package updates and bundled plugin runtime-dependency repairs when the target volume appears low on disk space, without blocking installs on best-effort filesystem checks. Fixes #71835. Thanks @abhinas90 and @jsompis.</li>
<li>Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin.</li>
<li>Gateway/Linux: include fnm <code>aliases/default/bin</code> in generated service PATHs and let doctor accept either modern fnm aliases or the legacy <code>current/bin</code> symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott.</li>
<li>Installer/Linux: run apt installs with noninteractive dpkg and needrestart settings so fresh Ubuntu 24.04 <code>curl | bash</code> installs do not hang while installing Node.js, Git, or build tools. Fixes #41146. Thanks @iht76, @alexcarv318, @cs3gallery, @firofame, and @cgdusek.</li>
<li>Providers/Bedrock: defer the AWS SDK import until Bedrock discovery actually runs so plugin registration and setup stay lightweight on cold start. Fixes #71690. Thanks @jarvis-ai-gregmoser.</li>
<li>Installer/macOS: stop immediately when Homebrew <code>node@24</code> installation fails and avoid printing PATH advice for missing Homebrew Node installs. Fixes #70411. Thanks @1fanwang.</li>
<li>WhatsApp: remove ack reactions after a visible reply when <code>messages.removeAckAfterReply</code> is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken.</li>
<li>Providers/Z.AI: map OpenClaw thinking controls to Z.AI's <code>thinking</code> payload and add opt-in preserved thinking replay via <code>params.preserveThinking</code>, so GLM 5.x can keep prior <code>reasoning_content</code> when requested. Fixes #58680. Thanks @xuanmingguo.</li>
<li>Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.</li>
<li>Plugins: scope setup and web-provider metadata manifest reads to explicit plugin ids when callers already know the owning plugin set. Thanks @vincentkoc.</li>
<li>Plugins/onboarding: defer onboarding install-record index writes until the guarded config commit so setup failures cannot leave the plugin index ahead of <code>openclaw.json</code>. Thanks @shakkernerd.</li>
<li>Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd.</li>
<li>Config/providers: accept <code>video</code> and <code>audio</code> in configured model <code>input</code> values and preserve them in provider catalog entries. Fixes #20721. Thanks @alvinttang.</li>
<li>Models/auth: honor the parent <code>--agent</code> flag for auth write commands (<code>add</code>, <code>login</code>, <code>setup-token</code>, <code>paste-token</code>, and the GitHub Copilot shortcut) so OAuth/API-key/token results are written to the requested agent store instead of the default agent. Fixes #71864. (#71933) Thanks @balric-seo.</li>
<li>TTS: strip model-emitted TTS directives from streamed block text before channel delivery, including directives split across adjacent blocks, while preserving the accumulated raw reply for final-mode synthesis. Fixes #38937.</li>
<li>TTS: keep explicit <code>provider=...</code> directive keys scoped to that provider and warn on unsupported keys instead of letting another speech provider consume overlapping keys. Fixes #60131.</li>
<li>TTS/Feishu: normalize final-mode streamed TTS-only audio before delivery so generated voice-note files use the same safe media path and native voice routing as normal final replies. Fixes #71920.</li>
<li>Feishu: transcribe inbound voice-note audio with the shared media audio path before agent dispatch and keep raw Feishu <code>file_key</code> payloads out of message text. Fixes #67120 and #61876.</li>
<li>Tasks: terminalize async Gateway agent task records from the Gateway run result while preserving aborted, failed, and cancelled outcomes instead of leaving completed runs stuck as active or lost. (#71905) Thanks @likewen-tech.</li>
<li>WhatsApp: let authorized group voice-note transcripts satisfy mention gating before reply dispatch, while keeping unmentioned transcripts in pending group history. Fixes #44908.</li>
<li>Media understanding: carry channel voice-note preflight state into attachment selection so WhatsApp, Feishu, Telegram, and Discord do not transcribe the same inbound audio twice. Fixes #70580.</li>
<li>TTS/BlueBubbles: deliver compatible auto-TTS audio as iMessage voice memo bubbles instead of plain MP3/CAF file attachments. Fixes #16848.</li>
<li>TTS: resolve voice-note and voice-memo routing from channel plugin capabilities instead of speech-core-owned channel id lists.</li>
<li>ACP: send subagent and async-task completion wakes to external ACP harnesses as plain prompts instead of OpenClaw internal runtime-context envelopes, while keeping those envelopes out of ACP transcripts.</li>
<li>TTS/status: show configured TTS model, voice, and sanitized custom endpoint in <code>/status</code>, preserve OpenAI-compatible TTS instructions on custom endpoints, and retry empty Microsoft/Edge TTS output once. Addresses #46602, #47232, and #43936. Thanks @leekuangtao, @Huntterxx, and @rex993.</li>
<li>Agents/Gateway: steer agent-driven config edits and restarts through the owner-only <code>gateway</code> tool, document <code>config.schema.lookup</code> as the field-doc source, and warn against using <code>gateway stop && gateway start</code> as a restart substitute on macOS. Fixes #71929. Thanks @ygc3817922006-sketch.</li>
<li>Media understanding/audio: inject a deterministic transcript placeholder for too-small voice notes so agents do not hallucinate transcription or provider failures. Fixes #48944. Thanks @eulicesl.</li>
<li>Providers/vLLM: send Nemotron 3 chat-template kwargs when thinking is off and honor configured <code>params.chat_template_kwargs</code> for OpenAI-compatible completions, so vLLM/Nemotron replies stay visible instead of becoming thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch.</li>
<li>Channels/replies: strip copied inbound metadata blocks from user-facing assistant replies and model replay history, so Discord/vLLM sessions do not leak <code>Conversation info</code> / <code>UNTRUSTED ... message body</code> envelopes after a model echoes them. Fixes #71847. Thanks @jmystaki-create.</li>
<li>Subagents/memory: keep inter-session completion wakes out of memory and dreaming session exports, and strip internal runtime-context blocks from realtime Control UI chat events.</li>
<li>Agents/Claude: treat zero-token empty <code>stop</code> turns as failed provider output, retry once, repair replay, and allow configured model fallback instead of preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI.</li>
<li>Tasks: normalize task lifecycle timestamps at create, update, and restore time, and report retained lost tasks as audit warnings until their cleanup window expires. (#71871) Thanks @likewen-tech.</li>
<li>Diagnostics/OTEL: treat normal early model stream cleanup as a completed model call instead of exporting a misleading <code>StreamAbandoned</code> error span. Thanks @vincentkoc.</li>
<li>Gateway/pairing: stop corrupt or unreadable device/node pairing stores from being treated as empty state, preserving <code>paired.json</code> for repair instead of overwriting approved pairings. Fixes #71873. Thanks @iret77.</li>
<li>ACP: keep <code>/acp</code> management commands, plus local <code>/status</code> and <code>/unfocus</code>, on the Gateway path inside ACP-bound threads so they are not consumed as ACP prompt text. Fixes #66298. Thanks @kindomLee.</li>
<li>ACPX: stop probing ACP agents during normal Gateway startup; the embedded backend now registers without spawning Codex/ACP child processes unless <code>OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=1</code> is explicitly set.</li>
<li>CLI/image edit: accept <code>--size</code>, <code>--aspect-ratio</code>, and <code>--resolution</code> on <code>openclaw infer image edit</code> and report all supported edit flags from <code>capability inspect image.edit</code>. Thanks @Pinghuachiu.</li>
<li>ACP: wait for the configured runtime backend to become healthy before startup identity reconciliation, avoiding transient acpx warnings during Gateway boot. Fixes #40566.</li>
<li>Channels/ACP bindings: time out configured binding readiness checks instead of letting Discord preflight hang forever when an ACP target never settles. Fixes #68776.</li>
<li>Control UI: hide the chat loading skeleton during background history reloads when existing messages or active stream content are already visible, avoiding reload flashes on high-latency local gateways. Fixes #71844. Thanks @WolvenRA.</li>
<li>Control UI: keep locally optimistic chat messages visible when a history reload temporarily returns empty, avoiding lost first-turn messages on high-latency gateways. Fixes #71878. Thanks @WolvenRA.</li>
<li>Control UI: keep chat history limits based on visible messages after filtering heartbeat and control-only transcript rows, so recent hidden entries no longer make older visible replies disappear. Thanks @WolvenRA.</li>
<li>Agents/images: scrub old <code>[media attached: ...]</code>, <code>[Image: source: ...]</code>, and <code>media://inbound/...</code> markers from pruned model replay context so stale media refs are not rehydrated as fresh prompt images. Fixes #71868. Thanks @jmeadlock.</li>
<li>Docker/Bonjour: disable Bonjour/mDNS advertising by default for bundled Compose gateways on bridge networking, while keeping host/macvlan opt-in with <code>OPENCLAW_DISABLE_BONJOUR=0</code>. Fixes #71879. Thanks @gbballpack.</li>
<li>CLI/status: label the OpenClaw Serve/Funnel setting as <code>Tailscale exposure</code> and show daemon state separately when available, so <code>gateway.tailscale.mode: "off"</code> no longer reads like the Tailscale daemon is stopped. Fixes #71790. Thanks @pesvobodak.</li>
<li>Plugins/Bonjour: stop ciao mDNS watchdog failures from looping forever when the advertiser stays stuck in <code>probing</code> or <code>announcing</code>; Bonjour now disables itself for the current Gateway process after repeated failed restarts while the Gateway keeps running. Fixes #69011. Thanks @siddharthaagarwalofficial-ux, @FiredMosquito831, and @spikefcz.</li>
<li>Gateway/Fly.io: seed Control UI allowed origins from the actual runtime bind and port so CLI-driven non-loopback starts do not crash before config exists. Fixes #71823.</li>
<li>macOS/remote SSH: keep discovered gateway hosts in <code>gateway.remote.sshTarget</code> while pinning SSH transport URLs to the local loopback tunnel, so browser automation does not regress into blocked non-loopback <code>ws://</code> endpoints. Fixes #67336.</li>
<li>Gateway/proxy: bootstrap env proxy dispatching from direct Gateway startup so provider and plugin network requests honor <code>HTTPS_PROXY</code>/<code>HTTP_PROXY</code> before the first embedded agent attempt runs. (#71833) Thanks @mjamiv.</li>
<li>Plugins/runtime deps: verify clean npm installs actually place requested bundled runtime packages in the managed install root, reporting exact missing specs instead of a false successful repair. (#71883) Thanks @Solvely-Colin.</li>
<li>Plugins/discovery: ignore stale <code>plugins.load.paths</code> aliases that point back at packaged bundled plugin directories and have doctor remove them, keeping bundled plugins on the runtime-deps staging path.</li>
<li>Models/LM Studio: preserve <code>@iq*</code> quant suffixes in model refs and provider matching so <code>/model lmstudio/...@iq3_xxs</code> keeps the exact LM Studio variant. Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.</li>
<li>Matrix/cron: preserve the live Matrix delivery target when creating implicit announce reminder jobs so mixed-case room IDs are not reconstructed from lowercased session keys. Fixes #71798.</li>
<li>Feishu: accept Schema 2.0 card action callbacks that report <code>context.open_chat_id</code> instead of legacy <code>context.chat_id</code>, so button callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.</li>
<li>Feishu: keep synthetic card-action and bot-menu ids out of platform reply targets, using the real card callback message id when Feishu provides one and plain-sending otherwise. Fixes #71673. Thanks @eddy1068.</li>
<li>Plugins/QQ Bot: prefer an installed QQ Bot plugin that declares it replaces the bundled <code>qqbot</code> channel, preventing duplicate <code>qqbot_channel_api</code> and <code>qqbot_remind</code> tool registration noise. Fixes #63102.</li>
<li>Browser automation: keep stable tab ids and labels attached when Chromium replaces the raw target after form submissions or other action-triggered navigations, and return the replacement <code>targetId</code> from <code>/act</code> when the match is provable. Fixes #46137.</li>
<li>QQ Bot: make <code>qqbot_remind</code> schedule, list, and remove Gateway cron jobs directly for owner-authorized senders instead of returning <code>cronParams</code> and relying on a follow-up generic <code>cron</code> tool call. Fixes #70865. (#70937) Thanks @GaosCode.</li>
<li>Agents/ACP: hide <code>sessions_spawn</code> ACP runtime options unless an ACP backend is loaded, and make <code>/acp doctor</code> call out <code>plugins.allow</code> blocking bundled <code>acpx</code>. Thanks @vincentkoc.</li>
<li>Agents/Codex: keep ACP prompt/skill routing hidden unless an ACP runtime backend is available, and warn in doctor when enabled Codex plugin configs still route <code>openai-codex/*</code> models through PI. Thanks @vincentkoc.</li>
<li>Media delivery: avoid sending generated image attachments twice when the assistant reply already includes explicit <code>MEDIA:</code> lines for the same turn, and reject unsafe remote <code>MEDIA:</code> URLs before delivery. Thanks @pashpashpash.</li>
<li>Codex harness: ignore retryable app-server error notifications after Codex recovers, and preserve the real nested error message for terminal app-server failures instead of replacing it with a generic failure. Thanks @pashpashpash.</li>
<li>Agents/Codex: prepare native Codex sub-agent session metadata without a nested Gateway session patch and add a focused Docker smoke for the app-server sub-agent path. Thanks @vincentkoc.</li>
<li>Agents/subagents: keep queued subagent announces session-only when the requester has no external channel target, avoiding ambiguous multi-channel delivery failures. Fixes #59201. Thanks @larrylhollan.</li>
<li>Image understanding: preserve configured provider-prefixed vision model metadata when callers request the model without the provider prefix, so custom image models keep their <code>input: ["text", "image"]</code> capability. Fixes #33185. Thanks @Kobe9312 and @vincentkoc.</li>
<li>Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.</li>
<li>Plugins/install: reject native plugin archives that do not include a valid <code>openclaw.plugin.json</code>, preventing manifestless archives from writing install records that later show missing-manifest diagnostics. Thanks @shakkernerd.</li>
<li>Plugins/uninstall: remove tracked managed plugin install directories even when the persisted install path differs from the default id-derived target, while still refusing deletes outside the managed extensions root. Thanks @shakkernerd.</li>
<li>Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.</li>
<li>Plugins/onboarding: defer channel/provider plugin install records until the owning config write commits, keeping setup failures from advancing the plugin index ahead of <code>openclaw.json</code>. Thanks @shakkernerd.</li>
<li>Plugins/config: route configure and agent setup writes with pending plugin install records through the plugin index commit helper so provider onboarding metadata is not stripped by plain config writes. Thanks @shakkernerd.</li>
<li>Plugins/channels: merge pending channel plugin install records with the existing plugin index before config writes, preserving unrelated tracked installs during channel setup, resolve, remove, and capability repair flows. Thanks @shakkernerd.</li>
<li>Plugins/config: defer shipped <code>plugins.installs</code> index migration during config writes until the guarded config commit window and roll it back if the config write fails before commit. Thanks @shakkernerd.</li>
<li>Sessions: keep embedded runtime context out of the visible user prompt by sending it as a hidden next-turn custom message, and teach doctor to repair affected 2026.4.24 transcripts with duplicated prompt-rewrite branches. Fixes #71761.</li>
<li>Gateway/subagents: keep direct-loopback backend RPCs authenticated with the shared gateway token/password off stale CLI paired-device scope baselines, so internal calls no longer hit <code>scope-upgrade</code> pairing prompts while remote, browser, node, device-token, and explicit-device paths still require normal pairing approval. Fixes #63548.</li>
<li>Providers/Azure OpenAI: give deployment-scoped image generation requests a longer 600s default timeout so slow <code>gpt-image-2</code> generations can complete without a per-call <code>timeoutMs</code>. Fixes #71705. Thanks @voytas75.</li>
<li>Gateway/plugins: link source-checkout bundled runtime dependency caches instead of recursively copying <code>node_modules</code> on the gateway main thread, preventing local status, node, and skill probes from timing out during startup cache restores.</li>
<li>Skills/remote nodes: only expose remote macOS skill bins for connected nodes, clear stale bin matches when node probes fail, and include probe command, timeout, bin count, and connection state in timeout logs.</li>
<li>Skills/remote nodes: recognize <code>system.which</code> object-map responses when probing connected macOS nodes, so Linux gateways can expose macOS-only skills such as Apple Notes when the required binaries are installed remotely. Fixes #71877. Thanks @miguelarios.</li>
<li>CLI/gateway: keep diagnostic probes from creating first-time read-only device pairings, while still reusing cached device tokens for detailed read probes. Fixes #71766. Thanks @SunboZ.</li>
<li>CLI/plugins: keep <code>message</code> startup, <code>channels logs</code>, <code>agents delete</code>, and <code>agents set-identity</code> off broad plugin preloading; message delivery still loads plugins when the action actually runs.</li>
<li>Image understanding: resolve configured image models such as local LM Studio vision entries before reporting <code>Unknown model</code> when the discovery registry has not registered that provider. Fixes #66486. Thanks @zhanggpcsu.</li>
<li>QQ Bot: ignore self-echoed bot messages using the outbound ref-index marker, preventing mirrored replies from re-entering the agent loop while still allowing users to quote bot replies. Fixes #71912. Thanks @wangyc6003.</li>
<li>Sessions: separate reset freshness from session-store <code>updatedAt</code>, so heartbeat, cron, exec, and gateway bookkeeping no longer prevent configured daily/idle resets from rolling long-running channel sessions. Fixes #68315, #63732, #63820, and #69083. Thanks @maxatv, @longhairedsi, @bradfreels, and @akessel56.</li>
<li>Sessions: clear queued system-event notices during <code>/new</code>, <code>/reset</code>, gateway <code>sessions.reset</code>, and daily/idle rollover so stale background updates cannot leak into the first prompt of the fresh session. Fixes #66864. Thanks @opeyio, @Magicray1217, and @cedillarack.</li>
<li>CLI/agents: keep <code>agents bind</code>, <code>agents unbind</code>, and <code>agents bindings</code> on setup-safe channel metadata paths so they do not preload bundled plugin runtimes or stage runtime dependencies. Fixes #71743.</li>
<li>Plugins/registry: preserve explicit disabled plugin records during registry migration without persisting every unused bundled plugin discovered on disk. Thanks @shakkernerd.</li>
<li>Windows/native: keep CLI startup and bundled provider plugin loading off Windows ESM raw-path failure paths, fixing native onboarding/install smoke on Node 24.</li>
<li>Plugins/doctor: read bundled channel doctor capabilities through the same packaged plugin directory resolver used by plugin loading, so published installs keep Matrix DM allowlist repairs on <code>channels.matrix.dm.*</code> instead of writing invalid top-level <code>dmPolicy</code> keys. Fixes #71757.</li>
<li>Plugins/Windows: keep bundled plugin Jiti loaders off the native import path on Windows so channel plugins such as Telegram no longer crash with <code>ERR_UNSUPPORTED_ESM_URL_SCHEME</code> on <code>C:\...</code> paths. Fixes #71749. Thanks @smeyer9.</li>
<li>Providers/Ollama: use Ollama's current <code>/api/web_search</code> endpoint and honor <code>https://ollama.com</code> model-provider base URLs for Ollama Web Search. Fixes #71741. Thanks @madhvidua.</li>
<li>Memory/Ollama: serialize Ollama memory embedding batches and add an inline batch timeout override, with longer defaults for local/self-hosted embedding providers.</li>
<li>Sessions/usage: exclude compaction checkpoint transcript snapshots from usage totals and session discovery, while keeping old checkpoint files removable.</li>
<li>CLI/agents: keep <code>openclaw agents list --json</code> on the config-only path by default, avoiding bundled plugin loading unless callers request <code>--bindings</code>. Fixes #71739. Thanks @kaloster.</li>
<li>Plugins/install: force plugin dependency installs to stay project-local even when inherited npm config requests global installs, so successful installs still materialize the plugin's staged <code>node_modules</code>.</li>
<li>Providers/Google: transcode Gemini TTS PCM to Opus for voice-note targets so WhatsApp and other native voice-note replies can play as voice messages.</li>
<li>TTS/WhatsApp: mark non-Opus provider output as voice-note intent so channel delivery transcodes MP3/WebM replies to Ogg/Opus PTT audio.</li>
<li>Plugins/runtime deps: reuse existing external bundled-plugin stage roots when mirrored plugin roots are inspected again, avoiding second-generation <code>openclaw-unknown-*</code> stages and repeated first-turn restaging. Fixes #71599.</li>
<li>iOS/macOS Talk Mode: allow <code>talk.speechLocale</code> to set the speech recognition locale for non-English voice conversations. Fixes #44688.</li>
<li>Plugins/providers: honor explicit plugin candidate lists instead of reading a persisted registry snapshot from local state, keeping candidate-scoped provider discovery hermetic.</li>
<li>Plugins/doctor: keep bundled plugin runtime-dependency repairs inside the managed OpenClaw stage even when user npm prefix/global config points npm at <code>$HOME/node_modules</code>. Fixes #71730.</li>
<li>ACP/sessions_spawn: reject normal OpenClaw config agent ids when callers explicitly request <code>runtime="acp"</code>, while allowing agents configured with <code>runtime.type="acp"</code> to resolve to their ACP harness id. Fixes #63914.</li>
<li>ACP/sessions_spawn: apply <code>runTimeoutSeconds</code> to ACP child turns and dispatch those turns on the background subagent lane, so quota-stalled ACP harnesses do not occupy the main agent lane indefinitely. Fixes #68823.</li>
<li>ACP/oneshot: reconcile runtime session identity before closing completed oneshot ACP runs, so finished <code>sessions.json</code> entries do not stay stuck with <code>acp.identity.state="pending"</code>.</li>
<li>ACPX: bundle <code>acpx@0.6.1</code> so unsupported generic model overrides fail clearly instead of silently falling back to the target adapter default.</li>
<li>ACP/models: document that non-Codex ACP model overrides require adapter support for ACP <code>models</code> plus <code>session/set_model</code>, so unsupported harnesses fail clearly instead of silently falling back to their defaults.</li>
<li>Plugins/Voice Call: treat missing provider credentials as setup-incomplete during Gateway startup and log the missing keys as a warning instead of a runtime startup error, while keeping explicit command/tool errors when used.</li>
<li>Android/Talk Mode: prevent duplicate TTS playback when fast or repeated final chat events arrive while Talk Mode is waiting for its own response. Fixes #46546.</li>
<li>Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so <code>pnpm check:changed</code> no longer waits on its own <code>lint:extensions</code> child.</li>
<li>CLI/completion: dedupe provider auth flags before registering <code>openclaw onboard</code> options, so completion-cache refresh during update no longer fails when stale core fallback flags overlap plugin manifest flags. Fixes #71667.</li>
<li>Diagnostics/trace: report live context usage from the current prompt snapshot instead of provider turn totals, avoiding false near-full context spikes on cached or tool-heavy runs.</li>
<li>Providers/Google: honor <code>models.providers.google.request.allowPrivateNetwork</code> for Gemini TTS and telephony TTS, matching Google image generation and media understanding. (#71723) Thanks @ro-hansolo.</li>
<li>Providers/MiniMax: register <code>minimax-portal</code> for music and video generation, preserving OAuth auth and regional MiniMax base URLs across the shared <code>music_generate</code> and <code>video_generate</code> tools. (#63241) Thanks @tars90percent.</li>
<li>Providers/onboarding: keep Runway and Alibaba Model Studio out of the text-inference setup picker by scoping their video-generation auth choices to the media setup flow. (#65856) Thanks @Jah-yee.</li>
<li>Plugins/Bonjour: stop the gateway from crash-looping on <code>CIAO PROBING CANCELLED</code> when the mDNS watchdog cancels a stuck probe. Restores the rejection-handler wiring dropped during the bonjour plugin migration and shares unhandled-rejection state across module instances so plugin-staged copies of <code>openclaw/plugin-sdk/runtime</code> register into the same handler set the host consults. Especially affects Docker on macOS, where mDNS probing reliably hits the watchdog. Thanks @troyhitch.</li>
<li>Google Meet: report pinned Chrome nodes as offline or missing capabilities in setup/join diagnostics, keep inaccessible nodes out of auto-selection, and preflight local BlackHole/SoX requirements before agents try local Chrome.</li>
<li>Providers/MiniMax: route <code>image-01</code> requests to the dedicated image generation endpoint while preserving CN endpoint selection. Fixes #61149. Thanks @mushuiyu886.</li>
<li>Plugins/startup: remove ownerless bundled runtime-dependency install locks after a short grace window and include lock owner details when startup times out waiting for a plugin runtime-deps lock.</li>
<li>Plugins/install: anchor bundled runtime-dependency npm installs with an OpenClaw-owned package manifest so Linux updates cannot accidentally write to a parent <code>$HOME/node_modules</code> tree. Fixes #71730.</li>
<li>Plugins/install: pass onboarding plugin config into plugin index writes so local plugin installs outside default discovery roots keep their install records. Thanks @shakkernerd.</li>
<li>Plugins/install: migrate shipped <code>plugins.installs</code> config records into the plugin index while stripping them from runtime config and future writes. Thanks @shakkernerd.</li>
<li>Plugins/install: durably remove shipped <code>plugins.installs</code> from <code>openclaw.json</code> after its records are copied into the plugin index, while rolling back the index write if config cleanup fails. Thanks @shakkernerd.</li>
<li>Plugins/install: keep migrated plugin install records in the plugin index even when the plugin manifest is missing or invalid, so update, uninstall, inspect, and audit can still recover broken installs. Thanks @shakkernerd.</li>
<li>Plugins/security: keep plugin audit JSON check ids stable while reporting plugin index install-record findings with updated wording. Thanks @shakkernerd.</li>
<li>CLI/config: reject direct <code>plugins.installs</code> edits with guidance to use <code>openclaw plugins install</code>, <code>openclaw plugins update</code>, or <code>openclaw plugins uninstall</code> instead. Thanks @shakkernerd.</li>
<li>Live tests/voice: accept common STT variants for OpenClaw and ElevenLabs brand names so provider smoke tests fail on real regressions rather than equivalent transcripts.</li>
<li>Agents/replies: forward sanitized underlying agent failure details on external channels instead of replacing unknown failures with a generic retry message.</li>
<li>CLI/MCP: translate OpenClaw <code>mcp.servers.*.transport</code> entries into Claude/Gemini CLI <code>type</code> fields so streamable HTTP MCP servers load in CLI backend sessions. (#71724) Thanks @Blockchain-Oracle.</li>
<li>Browser/CDP: honor configured remote and <code>attachOnly</code> CDP HTTP/WebSocket timeouts when opening tabs through raw CDP or <code>/json/new</code> fallback. (#54238) Thanks @FuncWei.</li>
<li>WhatsApp/TTS: send visible text separately from PTT voice-note audio instead of relying on hidden voice-note captions. Fixes #51081.</li>
<li>Browser/client: avoid telling agents to restart OpenClaw for dispatcher timeouts on external browser profiles such as <code>attachOnly</code>, remote CDP, and existing-session. (#40815) Thanks @0xsline.</li>
<li>Agents/TTS: preserve <code>[[audio_as_voice]]</code> directives on trusted text tool-result <code>MEDIA:</code> payloads so generated audio still delivers as a voice note. (#46535) Thanks @azade-c.</li>
<li>Agents/TTS: keep queued tool media when an assistant ends with <code>NO_REPLY</code> on non-block delivery paths, so media-only generated audio replies still send. (#60025) Thanks @bradlind1.</li>
<li>Telegram/STT: frame inbound voice-note transcripts as machine-generated, untrusted text in agent context while preserving raw transcript mention detection. Closes #33360. Thanks @smartchainark.</li>
<li>Subagents/browser: show an actionable <code>/tools</code> notice when browser automation is configured but filtered out by the active tool profile, and document that coding-profile agents should use <code>tools.alsoAllow: ["browser"]</code> rather than subagent allowlists alone.</li>
<li>Control UI/Quick Settings: persist the assistant avatar override to browser local storage (mirroring the user avatar) so uploaded image data URLs no longer fail config validation with "Too big: expected string to have <=200 characters". Also lift the gateway-side <code>ui.assistant.avatar</code> length cap to match the user avatar size budget for non-UI clients writing the field directly. Thanks @BunsDev.</li>
<li>Plugin SDK: share diagnostic event subscriptions across duplicate source/dist module graphs so legacy root SDK imports still receive runtime diagnostic events.</li>
<li>Agents/Bedrock: prevent empty assistant stream-error turns from poisoning Converse replay by persisting, repairing, and replaying a non-empty fallback block. Fixes #71572. (#71627) Thanks @openperf.</li>
<li>Agents/Anthropic/Bedrock: strip thinking blocks with missing, empty, or blank replay signatures before provider conversion, falling back to non-empty omitted-reasoning text when needed so corrupted signed-thinking history no longer poisons subsequent turns. Fixes #45010. (#70054) Thanks @castaples.</li>
<li>Agents/Anthropic/Bedrock: preserve stripped thinking-only assistant replay turns with non-empty omitted-reasoning text so provider adapters keep strict user/assistant turn shape. Thanks @wujiaming88.</li>
<li>ACP/Codex: pass <code>sessions_spawn(runtime="acp")</code> model and thinking overrides into Codex ACP startup, normalize <code>openai-codex/*</code> refs and slash reasoning suffixes, and recognize managed Codex ACP wrapper commands without blocking current <code>gpt-5.5</code> sessions. Fixes #40393. (#71643) Thanks @91wan.</li>
<li>Browser/CDP: make readiness diagnostics use the same discovery-first fallback as reachability for bare <code>ws://</code> Browserless and Browserbase CDP URLs. Fixes #69532.</li>
<li>Browser/CDP: explain that loopback Browserless or other externally managed CDP services need <code>attachOnly: true</code> and matching Browserless <code>EXTERNAL</code> endpoint when reporting local port ownership conflicts, and fall back to the configured bare WebSocket root when a discovered Browserless endpoint rejects CDP. Fixes #49815.</li>
<li>Gateway/reload: preserve indefinite <code>gateway.reload.deferralTimeoutMs: 0</code> semantics for channel hot reload deferrals so active agent runs are not interrupted by a forced channel restart. (#71637) Thanks @Poo-Squirry.</li>
<li>Agents/tool results: cap persisted Pi tool-result details and strip hidden diagnostics before provider conversion, preventing large debug payloads from bloating session transcripts. (#71637) Thanks @Poo-Squirry.</li>
<li>ACP/OpenCode: update the bundled acpx runtime to 0.6.0 and cover the OpenCode ACP bind path in Docker live tests.</li>
<li>Providers/OpenCode Go: add DeepSeek V4 Pro and DeepSeek V4 Flash to the Go catalog while the bundled Pi registry catches up. Fixes #71587.</li>
<li>Providers/OpenCode Go: route DeepSeek V4 Pro/Flash through the OpenAI-compatible Go endpoint and suppress invalid <code>reasoning_effort: "off"</code> payloads, fixing tool-enabled requests for <code>opencode-go/deepseek-v4-flash</code>. Fixes #71683.</li>
<li>Plugins/model defaults: run Skill Workshop review, Active Memory recall, and session-memory slug generation on the configured agent default model instead of the hardcoded OpenAI SDK fallback when hook context lacks model metadata. Fixes #71659.</li>
<li>Providers/Venice: fill the required DeepSeek V4 <code>reasoning_content</code> placeholder for <code>venice/deepseek-v4-pro</code> and <code>venice/deepseek-v4-flash</code> replay turns without sending native DeepSeek <code>thinking</code> controls that Venice rejects. Fixes #71628.</li>
<li>Browser/existing-session: support per-profile Chrome MCP command/args, map <code>cdpUrl</code> to <code>--browserUrl</code> or <code>--wsEndpoint</code>, and avoid combining endpoint flags with <code>--userDataDir</code>. Fixes #47879, #48037, and #62706. Thanks @puneet1409, @zhehao, and @madkow1001.</li>
<li>Media/plugins: bound MIME sniffing and ZIP archive preflight before handing untrusted files to <code>file-type</code> or <code>jszip</code>, reducing parser CPU and memory exposure for attachments and ClawHub plugin archives. Thanks @vincentkoc.</li>
<li>Memory-host SDK: use trusted env-proxy mode for remote embedding and batch HTTP calls only when Undici will proxy that target, preserving SSRF DNS pinning for <code>ALL_PROXY</code>-only and <code>NO_PROXY</code> bypass cases. Fixes #52162. (#71506) Thanks @DhtIsCoding.</li>
<li>Gateway/dashboard: render Control UI and WebSocket links with <code>https://</code>/<code>wss://</code> when <code>gateway.tls.enabled=true</code>, including <code>openclaw gateway status</code>. Fixes #71494. (#71499) Thanks @deepkilo.</li>
<li>Agents/OpenAI-compatible: default proxy/local completions tool requests to <code>tool_choice: "auto"</code> when tools are present, so providers enter native tool-calling mode instead of replying with plain-text tool directives. (#71472) Thanks @Speed-maker.</li>
<li>OpenAI image generation: use <code>gpt-5.5</code> for the Codex OAuth responses transport instead of the retired <code>gpt-5.4</code> model, fixing 500s from ChatGPT Codex image generation. Fixes #71513. Thanks @baolongl.</li>
<li>OpenAI image generation: route transparent-background default-model requests to <code>gpt-image-1.5</code>, document the expected <code>image_generate</code> call shape, and keep Azure/custom OpenAI-compatible deployment names untouched.</li>
<li>Google video generation: download direct MLDev Veo <code>video.uri</code> results instead of passing them through the Files API path, fixing 404s after successful generation/polling. Fixes #71200. Thanks @panhaishan.</li>
<li>Google video generation: fall back to the REST <code>predictLongRunning</code> Veo endpoint for text-only SDK 404s while keeping reference image/video generation on the SDK path. Fixes #62309 and #63008. (#62343) Thanks @leoleedev.</li>
<li>MiniMax music generation: switch the bundled default model from the unsupported <code>music-2.5+</code> id to the current <code>music-2.6</code> API model. Fixes #64870 and addresses the music default from #62315. Thanks @noahclanman and @edwardzheng1.</li>
<li>Cron: record jobs interrupted by a gateway restart as failed at their original <code>runningAtMs</code>, skip unsafe startup replay, and disable interrupted one-shot jobs so they show a visible failure instead of silently disappearing or duplicating work. Fixes #59056, #61343, #63657, and #59301. Thanks @ponchoooPenguin, @daemic24, @myradon, and @hikiwibot.</li>
<li>Cron tool: recover flat top-level schedule shorthand such as <code>cron</code>, <code>tz</code>, and <code>staggerMs</code> before gateway validation, so model-generated cron add/update calls preserve cron jitter settings. Thanks @tyxben.</li>
<li>Cron: hydrate flat legacy job rows with top-level <code>cron</code>, <code>tz</code>, <code>session</code>, and <code>message</code> fields into canonical schedule, target, and payload objects before startup recomputes run times. Fixes #43351.</li>
<li>Agents/replies: let pending group chat history trigger bare mentioned turns without treating metadata-only inbound context as user input. Fixes #71489. (#71520) Thanks @SymbolStar.</li>
<li>Google media generation: strip a configured trailing <code>/v1beta</code> from Google music/video provider base URLs before calling the Google GenAI SDK, preventing doubled <code>/v1beta/v1beta</code> paths. Fixes #63240. (#63258) Thanks @Hybirdss.</li>
<li>Discord: restore direct-message voice-note preflight transcription and classify URL-only Ogg/Opus voice attachments as audio while skipping partial attachments without usable URLs. Fixes #61314 and #64803.</li>
<li>Plugins/build: copy bundled plugin skill trees into <code>dist-runtime</code>, broaden Windows symlink-copy fallbacks, and fingerprint runtime dependencies from <code>lstat</code> so symlink-like directory entries cannot crash staging.</li>
<li>Google Chat: preserve reply text when a typing indicator message is deleted or can no longer be updated, so media captions and first text chunks are resent instead of silently disappearing. (#71498) Thanks @colin-lgtm.</li>
<li>Cron: tolerate malformed legacy job rows in startup, main-session system-event payloads, and human-readable <code>cron list</code> output so missing <code>state</code>, <code>payload.text</code>, or display fields no longer crash the scheduler or CLI. Fixes #66016, #65916, #64137, #57872, #59968, #63813, #52804, and #43163. (#71509) Thanks @vincentkoc.</li>
<li>CLI/models: make <code>openclaw models scan</code> fall back to public OpenRouter free-model metadata when no <code>OPENROUTER_API_KEY</code> is configured, avoid config secret resolution for explicit <code>--no-probe</code> scans, and apply the scan timeout to the OpenRouter catalog request.</li>
<li>Feishu: keep streaming cards to one live card per turn, flush throttled card edits after meaningful text boundaries, and skip exact block/partial repeats so tool-heavy replies do not duplicate card output. Thanks @allan0509.</li>
<li>Feishu: finish the streaming-card duplicate closeout by stripping leaked reasoning tags, preserving cross-block partial snapshots, enabling topic-thread streaming cards, omitting the generic <code>main</code> card header, surfacing transient tool/compaction status, and cleaning streaming state after close failures. Thanks @sesame437, @Vicky-v7, @maoku-family, @Pengxiao-Wang, and @Maple778.</li>
<li>Telegram: recover incomplete partial-stream previews by falling back to a final send when an ambiguous final edit failure would otherwise retain a strict prefix of the answer. Fixes #71525. (#71554) Thanks @sahilsatralkar.</li>
<li>Control UI/chat: collapse assistant token/model context details behind an explicit Context disclosure and show full dates in message footers, making historical transcript timing clear without noisy default metadata. (#71337) Thanks @BunsDev.</li>
<li>OpenAI/Codex OAuth: explain <code>unsupported_country_region_territory</code> token-exchange failures with a proxy/region hint instead of surfacing a generic OAuth error. Fixes #51175. (#71501) Thanks @vincentkoc and @wulala-xjj.</li>
<li>Browser/Linux: fall back to headless mode for local managed profiles on hosts without a display server, while preserving explicit per-profile headed overrides and reporting the headless source. (#60953) Thanks @rrpsantos.</li>
<li>Telegram: remove the startup persisted-offset <code>getUpdates</code> preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.</li>
<li>Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave <code>getUpdates</code> silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb.</li>
<li>Subagents: fall back to direct completion delivery when the parent announce turn finishes without a visible payload, so child results still reach channel-backed requester sessions.</li>
<li>Subagents: tell parent agents to use <code>sessions_yield</code> while waiting for child completion events, preventing GPT-5 fast runs from ending silently after spawning workers.</li>
<li>Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.</li>
<li>Browser/CLI: lazy-load browser command groups and plugin runtime services so <code>openclaw browser --help</code> can render without loading the full browser automation stack. Fixes #65400. (#65460, #66640) Thanks @pandego and @Tianworld.</li>
<li>Browser/CLI: serve precomputed <code>openclaw browser --help</code> text from CLI startup metadata, avoiding the full plugin/config startup path for the common help invocation.</li>
<li>Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser.</li>
<li>Browser/Chrome: stop passing redundant <code>--disable-setuid-sandbox</code> when <code>browser.noSandbox</code> is enabled; <code>--no-sandbox</code> remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger.</li>
<li>Browser/client: stop telling agents to permanently avoid the browser after transient timeout or cancellation failures; keep the no-retry hint for persistent unavailable/rate-limit cases. (#46505) Thanks @jriff.</li>
<li>Browser/aria snapshots: bind <code>format=aria</code> <code>axN</code> refs to live DOM nodes through backend DOM ids when Playwright is available, so follow-up browser actions can use those refs without timing out. (#62434) Thanks @MrKipler.</li>
<li>Telegram: prevent duplicate in-process long pollers for the same bot token and add clearer <code>getUpdates</code> conflict diagnostics for external duplicate pollers. Fixes #56230. Thanks @Co-Messi.</li>
<li>Browser/Linux: detect Chromium-based installs under <code>/opt/google</code>, <code>/opt/brave.com</code>, <code>/usr/lib/chromium</code>, and <code>/usr/lib/chromium-browser</code> before asking users to set <code>browser.executablePath</code>. (#48563) Thanks @lupuletic.</li>
<li>Sessions/browser: close tracked browser tabs when idle, daily, <code>/new</code>, or <code>/reset</code> session rollover archives the previous transcript, preventing tabs from leaking past the old session. Thanks @jakozloski.</li>
<li>Sessions/forking: fall back to transcript-estimated parent token counts when cached totals are stale or missing, so oversized thread forks start fresh instead of cloning the full parent transcript. Thanks @jalehman.</li>
<li>OpenAI/Codex: send Codex Responses system prompts through top-level <code>instructions</code> while preserving the existing native Codex payload controls.</li>
<li>MCP/CLI: retire bundled MCP runtimes at the end of one-shot <code>openclaw agent</code> and <code>openclaw infer model run</code> gateway/local executions, so repeated scripted runs do not accumulate stdio MCP child processes. Fixes #71457. Thanks @spartoviMD.</li>
<li>OpenAI/Codex image generation: canonicalize legacy <code>openai-codex.baseUrl</code> values such as <code>https://chatgpt.com/backend-api</code> to the Codex Responses backend before calling <code>gpt-image-2</code>, matching the chat transport. Fixes #71460. Thanks @GodsBoy.</li>
<li>Control UI: make <code>/usage</code> use the fresh context snapshot for context percentage, and include cache-write tokens in the Usage overview cache-hit denominator. Fixes #47885. Thanks @imwyvern and @Ante042.</li>
<li>GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay so Copilot can validate encrypted reasoning payloads across requests. (#71448) Thanks @a410979729-sys.</li>
<li>GitHub Copilot: never rewrite connection-bound reasoning item IDs regardless of whether <code>encrypted_content</code> is present, fixing a 400 "Encrypted content item_id did not match" error with <code>gpt-5.3-codex</code> and future Codex models that fall through to the forward-compat catch-all with <code>reasoning: false</code>. Also recognize Codex-named models as reasoning-capable so they inherit the correct capability flags. Refs #68735. Thanks @InvalidPandaa.</li>
<li>Agents/replies: recover final-answer text when streamed assistant chunks contain only whitespace, preventing completed turns from surfacing as empty-payload errors. Fixes #71454. (#71467) Thanks @Sanjays2402.</li>
<li>Feishu/TTS: transcode voice-intent MP3 and other audio replies to Ogg/Opus before sending native Feishu audio bubbles, while keeping ordinary MP3 attachments as files. Fixes #61249 and #37868. Thanks @sg1416-zg and @ycjlb2023-peteryi.</li>
<li>WhatsApp/TTS: transcode MP3/WebM audio, including Microsoft Edge TTS output, to Ogg/Opus before sending PTT voice notes.</li>
<li>QQBot/TTS: honor plain <code>audioAsVoice</code> replies by synthesizing TTS to native QQ voice messages, and mark inbound voice-only messages as audio media without exposing raw voice paths to generic media context.</li>
<li>Providers/SenseAudio: add bundled SenseAudio batch audio transcription through <code>tools.media.audio</code> with <code>SENSEAUDIO_API_KEY</code> auth. (#66943) Thanks @Fl0rencess720.</li>
<li>Providers/MiniMax: let TTS use MiniMax portal OAuth and Token Plan credentials before falling back to <code>MINIMAX_API_KEY</code>, and include current TTS HD model ids. Fixes #55017. Thanks @zx15210404690-hash.</li>
<li>Telegram/webhook: acknowledge validated webhook updates before running bot middleware, keeping slow agent turns from tripping Telegram delivery retries while preserving per-chat processing lanes. Fixes #71392. Thanks @joelforsberg46-source.</li>
<li>MCP/config reload: hot-apply <code>mcp.*</code> changes by disposing cached session MCP runtimes, and dispose bundled MCP runtimes during gateway shutdown so removed <code>mcp.servers</code> entries reap child processes promptly. Fixes #60656. Thanks @xieyuanqing.</li>
<li>Active Memory: keep silent recall sub-agent billing/auth failures out of shared auth-profile cooldown state, so a Claude CLI extra-usage rejection cannot disable normal Claude-backed turns. Fixes #71284. (#71539) Thanks @vishutdhar and @obviyus.</li>
<li>Auth/Claude CLI: sync refreshed Claude CLI OAuth credentials into the managed auth profile so long-running Claude CLI runs stop falling back to stale OpenClaw snapshots. (#70902) Thanks @starvex.</li>
<li>Sessions: make <code>sessions_spawn(mode="session")</code> errors name usable alternatives when the current channel cannot bind subagent threads. Fixes #67400. (#67790) Thanks @stainlu.</li>
<li>Agents/Claude CLI: pass the OpenClaw system prompt through Claude's prompt-file flag so Windows runs avoid argv length failures without changing system prompt semantics. Fixes #69158. (#69211) Thanks @skylee-01, @cassioanorte, @Syu0, and @Stache73.</li>
<li>Agents/CLI sessions: bind <code>google-gemini-cli</code> session auth-epoch to the Google account identity in <code>~/.gemini/oauth_creds.json</code>, so Gemini-backed agents resume their conversation after gateway restart instead of minting a fresh session, and stale bindings are invalidated when the authenticated Google account changes. Fixes #70973. (#71076) Thanks @openperf.</li>
<li>Slack: stop treating user mentions in assistant-authored message edit blocks as sender attribution, preventing edited bot messages from spoofing a mentioned DM user. (#71700) Thanks @vincentkoc.</li>
<li>Codex: consume unauthorized bound conversation inbound claims before they can fall through to other claim handlers or enqueue Codex turns. (#71702) Thanks @vincentkoc.</li>
<li>Codex media understanding: require approval-checked app-server image turns while explicitly declining tool, file, permission, and elicitation approval requests for the bounded image worker. (#71703) Thanks @vincentkoc.</li>
<li>Agents/Claude CLI: allow large live <code>stream-json</code> JSONL lines up to the existing per-turn raw limit, preventing large Telegram, WebChat, MCP, and image turns from aborting on the old stdout buffer cap. Fixes #71793, #71080, and #70766. (#71897) Thanks @chacher86, @shivamgrover21, and @tpjordan.</li>
<li>Agents/Claude CLI: unwrap nested Claude result envelopes in CLI JSON output so delegated agent responses surface as final text instead of raw result JSON. (#66819) Thanks @mraleko.</li>
<li>Agents/Claude CLI: apply the configured 1M context window override to eligible Claude CLI Opus and Sonnet models when <code>context1m</code> is enabled. (#70863) Thanks @bidadh.</li>
<li>Models/status: report fresh Claude CLI native auth instead of stale stored <code>anthropic:claude-cli</code> profile expiry when local credentials are current. Fixes #71256. (#71332) Thanks @matthiasjanke and @neeravmakwana.</li>
<li>CLI backends: compact OpenClaw transcripts after over-budget CLI turns and reseed fresh CLI sessions from the compacted transcript instead of stale external resume state. Fixes #68329. (#71916) Thanks @obviyus.</li>
<li>Telegram: keep default tool progress messages visible when answer preview streaming is disabled. (#71825) Thanks @VACInc.</li>
<li>Configure/models: clear deselected model fallbacks when updating the model picker allowlist, including provider-scoped setup flows. (#71596) Thanks @rubencu.</li>
<li>Agents/streaming: strip namespaced <code><antml:thinking></code> reasoning tags from streamed assistant replies before user-visible text is emitted. (#69288) Thanks @xialonglee.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.25/OpenClaw-2026.4.25.zip" length="48125363" type="application/octet-stream" sparkle:edSignature="RnQ01wCFgupauUdwOFan+XPGZhBJi/w3sgJYA5EaasbeGrduDHBGw1e9Zj2Lqb4ud8e6Q+tRcJVfxh5KKSEIDg=="/>
</item>
<item>
<title>2026.4.24</title>
<pubDate>Sat, 25 Apr 2026 19:34:45 +0000</pubDate>
@@ -50,318 +689,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.24/OpenClaw-2026.4.24.zip" length="48033180" type="application/octet-stream" sparkle:edSignature="wxOfxadSZ/9iXMitaC6SA9J6YPZC3P2tkeK7HZPHzjUIlzQTvOl7EjR4aRyXzaYt1N1AK5ba+YhuCwEngrTdCQ=="/>
</item>
<item>
<title>2026.4.22</title>
<pubDate>Thu, 23 Apr 2026 15:18:00 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042290</sparkle:version>
<sparkle:shortVersionString>2026.4.22</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.22</h2>
<h3>Changes</h3>
<ul>
<li>Providers/xAI: add image generation, text-to-speech, and speech-to-text support, including <code>grok-imagine-image</code> / <code>grok-imagine-image-pro</code>, reference-image edits, six live xAI voices, MP3/WAV/PCM/G.711 TTS formats, <code>grok-stt</code> audio transcription, and xAI realtime transcription for Voice Call streaming. (#68694) Thanks @KateWilkins.</li>
<li>Providers/STT: add Voice Call streaming transcription for Deepgram, ElevenLabs, and Mistral, alongside the existing OpenAI and xAI realtime STT paths; ElevenLabs also gains Scribe v2 batch audio transcription for inbound media.</li>
<li>TUI: add local embedded mode for running terminal chats without a Gateway while keeping plugin approval gates enforced. (#66767) Thanks @fuller-stack-dev.</li>
<li>Onboarding: auto-install missing provider and channel plugins during setup so first-run configuration can complete without manual plugin recovery.</li>
<li>OpenAI/Responses: use OpenAI's native <code>web_search</code> tool automatically for direct OpenAI Responses models when web search is enabled and no managed search provider is pinned; explicit providers such as Brave keep the managed <code>web_search</code> tool.</li>
<li>Models/commands: add <code>/models add <provider> <modelId></code> so you can register a model from chat and use it without restarting the gateway; keep <code>/models</code> as a simple provider browser while adding clearer add guidance and copy-friendly command examples. (#70211) Thanks @Takhoffman.</li>
<li>WhatsApp: add configurable native reply quoting with replyToMode for WhatsApp conversations. Thanks @mcaxtr.</li>
<li>WhatsApp/groups+direct: forward per-group and per-direct <code>systemPrompt</code> config into inbound context <code>GroupSystemPrompt</code> so configured per-chat behavioral instructions are injected on every turn. Supports <code>"*"</code> wildcard fallback and account-scoped overrides under <code>channels.whatsapp.accounts.<id>.{groups,direct}</code>; account maps fully replace root maps (no deep merge), matching the existing <code>requireMention</code> pattern. Closes #7011. (#59553) Thanks @Bluetegu.</li>
<li>Agents/sessions: add mailbox-style <code>sessions_list</code> filters for label, agent, and search plus visibility-scoped derived title and last-message previews. (#69839) Thanks @dangoZhang.</li>
<li>Control UI/settings+chat: add a browser-local personal identity for the operator (name plus local-safe avatar), route user identity rendering through the shared chat/avatar path used by assistant and agent surfaces, and tighten Quick Settings, agent fallback chips, and narrow-screen chat layouts so personalization no longer wastes space or clips controls. (#70362) Thanks @BunsDev.</li>
<li>Gateway/diagnostics: enable payload-free stability recording by default and add a support-ready diagnostics export with sanitized logs, status, health, config, and stability snapshots for bug reports. (#70324) Thanks @gumadeiras.</li>
<li>Providers/Tencent: add the bundled Tencent Cloud provider plugin with TokenHub onboarding, docs, <code>hy3-preview</code> model catalog entries, and tiered Hy3 pricing metadata. (#68460) Thanks @JuniperSling.</li>
<li>Providers/Amazon Bedrock Mantle: add Claude Opus 4.7 through Mantle's Anthropic Messages route with provider-owned bearer-auth streaming, so the model is actually callable without treating AWS bearer tokens like Anthropic API keys. Thanks @wirjo.</li>
<li>Providers/GPT-5: move the GPT-5 prompt overlay into the shared provider runtime so compatible GPT-5 models receive the same behavior and heartbeat guidance through OpenAI, OpenRouter, OpenCode, Codex, and other GPT providers; add <code>agents.defaults.promptOverlays.gpt5.personality</code> as the global friendly-style toggle while keeping the OpenAI plugin setting as a fallback.</li>
<li>Providers/OpenAI Codex: remove the Codex CLI auth import path from onboarding and provider discovery so OpenClaw no longer copies <code>~/.codex</code> OAuth material into agent auth stores; use browser login or device pairing instead. (#70390) Thanks @pashpashpash.</li>
<li>CLI/Claude: default <code>claude-cli</code> runs to warm stdio sessions, including custom configs that omit transport fields, and resume from the stored Claude session after Gateway restarts or idle exits. (#69679) Thanks @obviyus.</li>
<li>Pi/models: update the bundled pi packages to <code>0.68.1</code> and let the OpenCode Go catalog come from pi instead of plugin-maintained model aliases, adding the refreshed <code>opencode-go/kimi-k2.6</code>, Qwen, GLM, MiMo, and MiniMax entries.</li>
<li>Tokenjuice: add bundled native OpenClaw support for tokenjuice as an opt-in plugin that compacts noisy <code>exec</code> and <code>bash</code> tool results in Pi embedded runs. (#69946) Thanks @vincentkoc.</li>
<li>ACPX: add an explicit <code>openClawToolsMcpBridge</code> option that injects a core OpenClaw MCP server for selected built-in tools, starting with <code>cron</code>.</li>
<li>CLI/doctor plugins: lazy-load doctor plugin paths and prefer installed plugin <code>dist/*</code> runtime entries over source-adjacent JavaScript fallbacks, reducing the measured <code>doctor --non-interactive</code> runtime by about 74% while keeping cold doctor startup on built plugin artifacts. (#69840) Thanks @gumadeiras.</li>
<li>CLI/debugging: add an opt-in temporary debug timing helper for local CLI performance investigations, with readable stderr output, JSONL capture, and docs for removing probes before landing fixes. (#70469) Thanks @shakkernerd.</li>
<li>Docs/i18n: add Thai translation support for the docs site.</li>
<li>Providers/OpenAI-compatible: mark known local backends such as vLLM, SGLang, llama.cpp, LM Studio, LocalAI, Jan, TabbyAPI, and text-generation-webui as streaming-usage compatible, so their token accounting no longer degrades to unknown/stale totals. (#68711) Thanks @gaineyllc.</li>
<li>Providers/OpenAI-compatible: recover streamed token usage from llama.cpp-style <code>timings.prompt_n</code> / <code>timings.predicted_n</code> metadata and sanitize usage counts before accumulation, fixing unknown or stale totals when compatible servers do not emit an OpenAI-shaped <code>usage</code> object. (#41056) Thanks @xaeon2026.</li>
<li>Plugins/startup: prefer native Jiti loading for built bundled plugin dist modules on supported runtimes, cutting measured bundled plugin load time by 82-90% while keeping source TypeScript on the transform path. (#69925) Thanks @aauren.</li>
<li>Plugin SDK/STT: share realtime transcription WebSocket transport and multipart batch transcription form helpers across bundled STT providers, reducing provider plugin boilerplate while preserving proxy capture, reconnects, audio queueing, close flushing, upload filename normalization, and ready handshakes.</li>
<li>Plugin SDK/Pi embedded runs: add a bundled-plugin embedded extension factory seam so native plugins can extend Pi embedded runs with async runtime hooks such as <code>tool_result</code> handling instead of falling back to the older synchronous persistence path. (#69946) Thanks @vincentkoc.</li>
<li>Codex harness/hooks: route native Codex app-server turns through <code>before_prompt_build</code> and emit <code>before_compaction</code> / <code>after_compaction</code> for native compaction items so prompt and compaction hooks stop drifting from Pi. Thanks @vincentkoc.</li>
<li>Codex harness/plugins: add a bundled-plugin Codex app-server extension seam for async <code>tool_result</code> middleware, fire <code>after_tool_call</code> for Codex tool runs, and route mirrored Codex transcript writes through <code>before_message_write</code> so tool integrations stop diverging from Pi. Thanks @vincentkoc.</li>
<li>Codex harness/hooks: fire <code>llm_input</code>, <code>llm_output</code>, and <code>agent_end</code> for native Codex app-server turns so lifecycle hooks stop drifting from Pi. Thanks @vincentkoc.</li>
<li>QA/Telegram: record per-scenario reply RTT in the live Telegram QA report and summary, starting with the canary response. (#70550) Thanks @obviyus.</li>
<li>Status: add an explicit <code>Runner:</code> field to <code>/status</code> so sessions now report whether they are running on embedded Pi, a CLI-backed provider, or an ACP harness agent/backend such as <code>codex (acp/acpx)</code> or <code>gemini (acp/acpx)</code>. (#70595)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy <code>off</code>/<code>low</code> fallback behavior to a safe provider-supported <code>medium</code> equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make <code>/status</code> report the same resolved default as runtime.</li>
<li>Gateway/model pricing: fetch OpenRouter and LiteLLM pricing asynchronously at startup and extend catalog fetch timeouts to 30 seconds, reducing noisy timeout warnings during slow upstream responses.</li>
<li>Agents/sessions: keep daily reset and idle-maintenance bookkeeping from bumping session activity or pruning freshly active routes, so active conversations no longer look newer or disappear for maintenance-only updates.</li>
<li>Plugins/install: add newly installed plugin ids to an existing <code>plugins.allow</code> list before enabling them, so allowlisted configs load installed plugins after restart.</li>
<li>Status: show <code>Fast</code> in <code>/status</code> when fast mode is enabled, including config/default-derived fast mode, and omit it when disabled.</li>
<li>OpenAI/image generation: detect Azure OpenAI-style image endpoints, use Azure <code>api-key</code> auth plus deployment-scoped image URLs, honor <code>AZURE_OPENAI_API_VERSION</code>, and document the Azure setup path so image generation and edits work against Azure-hosted OpenAI resources. (#70570) Thanks @zhanggpcsu.</li>
<li>Telegram/forum topics: cache recovered forum metadata with bounded expiry so supergroup updates no longer need repeated <code>getChat</code> lookups before topic routing.</li>
<li>Onboarding/WeCom: show the official WeCom channel plugin with its native Enterprise WeChat display name and blurb in the external channel catalog.</li>
<li>Models/auth: merge provider-owned default-model additions from <code>openclaw models auth login</code> instead of replacing <code>agents.defaults.models</code>, so re-authenticating an OAuth provider such as OpenAI Codex no longer wipes other providers' aliases and per-model params. Migrations that must rename keys (Anthropic -> Claude CLI) opt in with <code>replaceDefaultModels</code>. Fixes #69414. (#70435) Thanks @neeravmakwana.</li>
<li>Media understanding/audio: prefer configured or key-backed STT providers before auto-detected local Whisper CLIs, so installed local transcription tools no longer shadow API providers such as Groq/OpenAI in <code>tools.media.audio</code> auto mode. Fixes #68727.</li>
<li>Providers/OpenAI: lock the auth picker wording for OpenAI API key, Codex browser login, and Codex device pairing so the setup choices no longer imply a mixed Codex/API-key auth path. (#67848) Thanks @tmlxrd.</li>
<li>Agents/BTW: route <code>/btw</code> side questions through provider stream registration with the session workspace, so Ollama provider URL construction and workspace-scoped hooks apply correctly. Fixes #68336. (#70413) Thanks @suboss87.</li>
<li>Agents/sessions: make session transcript write locks non-reentrant by default, so same-process transcript writers contend unless a helper explicitly opts into nested lock ownership.</li>
<li>ACPX/probe: expose an optional <code>probeAgent</code> plugin config field so the embedded ACP runtime health probe can target a configured agent (for example <code>opencode</code> or <code>claude</code>) instead of hardcoding <code>codex</code>, and stop marking the entire ACP runtime backend unavailable when the default probe agent is simply not installed or not authenticated. (#68409) Thanks @lyfuci.</li>
<li>Memory search: use sqlite-vec KNN for vector recall while preserving full post-filter result limits in multi-model indexes. Fixes #69666. (#69680) Thanks @aalekh-sarvam.</li>
<li>Providers/OpenAI Codex: stop stale per-agent <code>openai-codex:default</code> OAuth profiles from shadowing a newer main-agent identity-scoped profile, and let <code>openclaw doctor</code> offer the matching cleanup. (#70393) Thanks @pashpashpash.</li>
<li>ACPX: route OpenClaw ACP bridge commands through the MCP-free runtime path even when the command is wrapped with <code>env</code>, has bridge flags, or is resumed from persisted session state, so documented <code>acpx openclaw</code> setups no longer fail on per-session MCP injection. (#68741) Thanks @alexlomt.</li>
<li>Codex harness: route Codex-tagged MCP tool approval elicitations through OpenClaw plugin approvals, including current empty-schema app-server requests, while leaving generic user-input prompts fail-closed. (#68807) Thanks @kesslerio.</li>
<li>WhatsApp/outbound: hold an in-memory active-delivery claim while a live outbound send is in flight, so a concurrent reconnect drain no longer re-drives the same pending queue entry and duplicates cron sends 7-12x after the 30-minute inbound-silence watchdog fires mid-delivery. Crash-replay of fresh queue entries left behind by a dead process is preserved because the claim is intentionally process-local. Fixes #70386. (#70428) Thanks @neeravmakwana.</li>
<li>Matrix/commands: keep Matrix DM allowlist state out of room control-command authorization, so trusted DM senders do not accidentally gain room-command access.</li>
<li>Providers/SDK retry: cap long <code>Retry-After</code> 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.</li>
<li>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 <code>MEDIA:</code> or voice tags as delivery metadata. (#68869) Thanks @zqchris.</li>
<li>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.</li>
<li>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 (<code>stopReason=stop</code>). Real failure modes (tool errors, provider <code>stopReason=error</code>, interrupted tool use) still surface the existing "verify before retrying" warning. Fixes #70396. (#70425) Thanks @neeravmakwana.</li>
<li>Gateway/Linux: wrap gateway-managed supervisor, PTY, MCP stdio, and browser child processes in a tiny <code>/bin/sh</code> shim that raises the child's own <code>oom_score_adj</code> on Linux, so under cgroup memory pressure the kernel prefers transient workers over the long-lived gateway. Opt out with <code>OPENCLAW_CHILD_OOM_SCORE_ADJ=0</code>. Fixes #70404. (#70419) Thanks @neeravmakwana.</li>
<li>Providers/Moonshot: stop strict-sanitizing Kimi's native tool_call IDs (shaped like <code>functions.<name>:<index></code>) 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 <code>sanitizeToolCallIds</code> opt-out to the shared <code>openai-compatible</code> replay family helper and wires Moonshot to it. Fixes #62319. (#70030) Thanks @LeoDu0314.</li>
<li>Dependencies/security: override transitive <code>uuid</code> to <code>14.0.0</code>, clearing the runtime advisory across dependencies.</li>
<li>Codex harness: ignore dynamic tool descriptions when deciding whether to reuse a native app-server thread while still fingerprinting tool schemas, so channel-specific copy changes no longer reset otherwise compatible Codex conversations. (#69976) Thanks @chen-zhang-cs-code.</li>
<li>Codex harness: expose the Codex app-server model catalog in <code>models list/status</code>, avoid startup hangs from app-server discovery timeouts, and accept current Codex turn-completion notifications so Docker live gateway turns finish reliably.</li>
<li>Codex harness: drop invalid legacy app-server <code>serviceTier</code> values such as <code>"priority"</code> before native thread and turn requests, while keeping supported Codex tiers limited to <code>"fast"</code> and <code>"flex"</code>. Fixes #64815.</li>
<li>Codex harness: show bounded, sanitized permission target samples in app-server approval prompts, so native permission requests keep their specific hosts, roots, and paths visible without leaking home usernames or URL credentials. (#70340) Thanks @Lucenx9.</li>
<li>Docs/Codex harness: narrow native compaction docs to the current start/completion signals, without promising a readable summary or kept-entry audit list yet. (#69612) Thanks @91wan.</li>
<li>Providers/Amazon Bedrock: use known context-window metadata for discovered models while keeping the unknown-model fallback conservative, so compaction and overflow handling improve for newer Bedrock models without overstating unlisted model limits. Thanks @wirjo.</li>
<li>Providers/Amazon Bedrock Mantle: refresh IAM-backed bearer tokens at runtime instead of baking discovery-time tokens into provider config, so long-lived Mantle sessions keep working after the initial token ages out. Thanks @wirjo.</li>
<li>Config/includes: write through single-file top-level includes for isolated OpenClaw-owned mutations, so <code>plugins install</code> and <code>plugins update</code> update an included <code>plugins.json5</code> file instead of flattening modular <code>$include</code> configs. Fixes #41050 and #66048.</li>
<li>Config/reload: plan gateway reloads from source-authored config instead of runtime-materialized snapshots, so plugin update writes no longer trigger false restarts from derived provider/plugin config paths. Fixes #68732.</li>
<li>Plugins/update: skip npm plugin reinstall/config rewrites when the installed version and recorded artifact identity already match the registry target, let bare npm package names resolve back to tracked install records, and point already-installed <code>plugins install</code> attempts at <code>plugins update</code> / <code>--force</code> instead of a hook-pack fallback. Fixes #46955, #67957, and #68073.</li>
<li>Agents/MCP: keep <code>mcp.servers</code> and bundle MCP tools available in Pi embedded <code>coding</code> and <code>messaging</code> sessions while preserving <code>minimal</code> profile and <code>tools.deny: ["bundle-mcp"]</code> opt-out behavior. Fixes #68875 and #68818.</li>
<li>Plugins/startup: tolerate transient bundled-channel catalog/metadata drift while auto-enabling configured plugins, so CLI and gateway startup no longer crash when a channel id is known but its display metadata is unavailable.</li>
<li>CLI/Claude: report CLI-backed reply runs as streaming while Claude/Codex CLI turns are still in flight, so WebChat keeps visible response state until the backend finishes. Fixes #70125.</li>
<li>Slack/streaming: fall back to normal Slack replies for Slack Connect streams rejected before the SDK flushes its local buffer, so short replies no longer disappear or report success before Slack acknowledges delivery. Fixes #70295. (#70370) Thanks @mvanhorn.</li>
<li>Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new <code>Authorization</code> header instead of reusing a stale socket. (#70328) Thanks @Lucenx9.</li>
<li>Channels/sandbox: derive runtime policy keys for external direct messages that share the main conversation, so sandbox/tool policy no longer treats channel-originated DMs as local main-session runs.</li>
<li>Config/models: merge provider-scoped model allowlist updates and protect model/provider map writes from accidental full replacement, adding <code>config set --merge</code> for additive updates and <code>--replace</code> for intentional clobbers. Fixes #65920, #68392, and #68653.</li>
<li>Agents/Pi auth: preserve AWS SDK-authenticated Bedrock runs for IMDS and task-role setups, clear stale refresh timers on sentinel fallback, and log unexpected runtime-auth prep failures instead of silently leaving the provider unauthenticated. Thanks @wirjo.</li>
<li>Config/gateway: restore last-known-good config on critical clobber signatures such as missing metadata, missing <code>gateway.mode</code>, or sharp size drops, preventing gateway crash loops when a valid backup exists. Fixes #70336.</li>
<li>Config/gateway: recover configs accidentally prefixed with non-JSON output during gateway startup or <code>openclaw doctor --fix</code>, preserving the clobbered file as a backup while leaving normal config reads read-only.</li>
<li>Agents/GitHub Copilot: normalize connection-bound Responses item IDs in the Copilot provider wrapper so replayed histories no longer fail after the upstream connection changes. (#69362) Thanks @Menci.</li>
<li>Pi embedded runs: pass real built-in tools into Pi session creation and then narrow active tool names after custom tool registration, so the runner and compaction paths compile cleanly and keep OpenClaw-managed custom tool allowlists without feeding string arrays into <code>createAgentSession</code>. Thanks @vincentkoc.</li>
<li>Agents/OpenAI websocket: route native OpenAI websocket metadata and session-header decisions through the shared endpoint classifier so local mocks and custom <code>models.providers.openai.baseUrl</code> endpoints stay out of the native OpenAI path consistently across embedded-runner and websocket transport code. Thanks @vincentkoc.</li>
<li>Cron/MCP: retire bundled MCP runtimes through one shared cleanup path for isolated cron run ends, persistent cron session rollover, and direct cron <code>deleteAfterRun</code> fallback cleanup. Fixes #69145, #68623, and #68827.</li>
<li>MCP/gateway: tear down stdio MCP process trees on transport close and dispose bundled MCP runtimes during session delete/reset, preventing orphaned wrapper/server processes from accumulating. Fixes #68809 and #69465.</li>
<li>Agents/MCP: retire bundled MCP runtimes after completed one-shot subagent cleanup and nested <code>sessions_send</code> steps, while keeping persistent subagent sessions warm.</li>
<li>Config: render validation warnings with real line breaks instead of a literal <code>\n</code> sequence in CLI/audit output. Fixes #70140.</li>
<li>Cron/doctor: repair malformed persisted cron job IDs through <code>openclaw doctor</code>, including legacy <code>jobId</code>, non-string <code>id</code>, and missing <code>id</code> rows, so <code>cron list</code> no longer needs display-layer coercion for corrupt store data. Fixes #70128.</li>
<li>Discord: normalize prefixed channel targets only at the thread-binding API boundary, so <code>sessions_spawn({ runtime: "acp", thread: true })</code> can create child threads from Discord channels without breaking current-channel ACP bindings. (#68034) Thanks @Zetarcos.</li>
<li>Discord: harden inbound thread metadata handling against partial Carbon channel getters, so non-command thread messages and queued jobs no longer crash when <code>name</code>, <code>parentId</code>, <code>parent</code>, or <code>ownerId</code> requires fetched raw data.</li>
<li>Discord: let <code>message</code> tool reactions resolve <code>user:<id></code> DM targets and preserve <code>channels.discord.guilds.<guild>.channels.<channel>.requireMention: false</code> during reply-stage activation fallback. Fixes #70165 and #69441.</li>
<li>Plugins/startup: pre-normalize and cache Jiti alias maps before creating plugin loaders, so module-scoped loader filenames do not reintroduce per-plugin alias-normalization startup cost. Fixes #70186.</li>
<li>ACP/Codex: run the bundled Codex ACP harness with an isolated <code>CODEX_HOME</code> and avoid writing incomplete ChatGPT auth bridge files, so Codex ACP sessions no longer clobber the user's real Codex CLI auth. Fixes #70234. Thanks @Lonobers88.</li>
<li>Gateway/client: keep long-running RPCs such as ACP <code>agent.wait</code> calls in charge of their own timeout instead of closing the websocket on a missed app-level tick while work is still pending.</li>
<li>Telegram/webhooks: lower the grammY webhook callback timeout to 5s so Telegram gets an early 200 response instead of retrying long-running updates as read timeouts. (#70146) Thanks @friday-james.</li>
<li>Telegram/polling: rebuild the polling HTTP transport after <code>getUpdates</code> 409 conflicts, so retries use a fresh TCP connection instead of looping on a Telegram-terminated keep-alive socket. (#69873) Thanks @hclsys.</li>
<li>Media delivery: strip persisted base64 audio payloads from webchat history, resolve stored <code>media://inbound/*</code> attachments before local-root checks, suppress duplicate Telegram voice/audio sends when TTS emits the same media twice, and support custom image-model IDs that already include their provider prefix.</li>
<li>Slack/files: resolve <code>downloadFile</code> bot tokens from the runtime config when callers provide <code>cfg</code> without an explicit token or prebuilt client, preserving cfg-only file downloads outside the action runtime path. (#70160) Thanks @martingarramon.</li>
<li>Slack/HTTP: dispatch registered Request URL webhooks through the same handler registry used by Slack monitor setup, so HTTP-mode Slack events no longer 404 after successful route registration. (#70275) Thanks @FroeMic.</li>
<li>Slack/runtime bindings: route focused Slack thread replies through their bound ACP session instead of preparing replies against the default agent shell. Fixes #67739. Thanks @Frankla20.</li>
<li>CLI/Claude: keep stored Claude CLI sessions through OAuth refresh-token rotation by keying auth epochs on stable account identity instead of mutable OAuth token material. (#70452) Thanks @obviyus.</li>
<li>CLI/Claude: verify stored Claude CLI session ids have a readable project transcript before resuming, clearing phantom bindings with <code>reason=transcript-missing</code> instead of silently starting fresh under <code>--resume</code>. Fixes #70177.</li>
<li>CLI sessions: persist CLI session clearing through the atomic session-store merge path, so expired Claude/Codex CLI bindings are actually removed before retrying without the stale session id. (#70298) Thanks @HFConsultant.</li>
<li>ACP/sessions_spawn: honor explicit <code>model</code> overrides for ACP child sessions instead of silently falling back to the target agent default model. (#70210) Thanks @felix-miao.</li>
<li>Diffs/viewer: re-read remote viewer access policy from live runtime config on each request, so toggling <code>plugins.entries.diffs.config.security.allowRemoteViewer</code> closes proxied viewer access immediately instead of waiting for a restart. Thanks @vincentkoc.</li>
<li>Diffs/tooling: re-read <code>viewerBaseUrl</code>, presentation defaults, and viewer access policy from live runtime config, and fail closed when the live <code>diffs</code> plugin entry disappears instead of reviving startup viewer settings. Thanks @vincentkoc.</li>
<li>Memory/LanceDB: stop resurrecting removed live <code>memory-lancedb</code> hook config from startup snapshots, so deleting or disabling the plugin entry shuts off auto-recall and auto-capture without a restart. Thanks @vincentkoc.</li>
<li>Memory/LanceDB: keep auto-recall and auto-capture hooks wired when those settings start disabled, so turning them on in live config starts recall and capture without waiting for a restart. Thanks @vincentkoc.</li>
<li>Skill Workshop: keep the tool plus <code>before_prompt_build</code> / <code>agent_end</code> hooks wired while the plugin is disabled at startup, so turning the plugin back on in live config starts guidance and capture without waiting for a restart. Thanks @vincentkoc.</li>
<li>Active Memory: stop reviving removed live <code>active-memory</code> config from startup snapshots, so removing the plugin entry turns the hook off immediately instead of waiting for a restart. Thanks @vincentkoc.</li>
<li>GitHub Copilot: re-read plugin discovery config from the live runtime snapshot, so toggling <code>plugins.entries.github-copilot.config.discovery.enabled</code> takes effect without a restart. Thanks @vincentkoc.</li>
<li>Ollama: re-read plugin discovery config from the live runtime snapshot, so toggling <code>plugins.entries.ollama.config.discovery.enabled</code> takes effect without a restart. Thanks @vincentkoc.</li>
<li>OpenAI: re-read the plugin prompt-overlay personality from live runtime config, so GPT-5 system prompt contributions update without a restart when <code>plugins.entries.openai.config.personality</code> changes. Thanks @vincentkoc.</li>
<li>Amazon Bedrock: re-read live discovery and guardrail plugin config, so toggling <code>plugins.entries.amazon-bedrock.config.discovery</code> or <code>plugins.entries.amazon-bedrock.config.guardrail</code> takes effect without a restart. Thanks @vincentkoc.</li>
<li>Codex: re-read the plugin discovery config from the live runtime snapshot, so toggling <code>plugins.entries.codex.config.discovery</code> takes effect without a restart. Thanks @vincentkoc.</li>
<li>Agents/subagents: drop bare <code>NO_REPLY</code> from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana.</li>
<li>Plugins/install: keep bundled plugin dependencies off npm install while repairing them when plugins activate from a packaged install, including Feishu/Lark, Browser, and direct bundled channel setup-entry loads.</li>
<li>CLI/channels: skip and cache bundled channel plugin, setup, and secrets load failures during read-only discovery, so one broken unused bundled channel cannot crash <code>openclaw status</code> or bootstrap secret scans.</li>
<li>Memory/LanceDB: retry initialization after a failed LanceDB load and report unsupported Intel macOS native runtime clearly instead of caching the failure or repeatedly attempting an install that cannot work.</li>
<li>CLI/Claude: hash only static extra system prompt parts when deciding whether to reuse a CLI session, so per-message inbound metadata no longer resets Claude CLI conversations on every turn. (#70122) Thanks @zijunl.</li>
<li>Hooks/Slack: standardize shared message hook routing fields (<code>threadId</code> / <code>replyToId</code>) and stop Slack outbound delivery from re-running <code>message_sending</code> inside the channel adapter, so plugins like thread-ownership make one outbound routing decision per reply. Thanks @vincentkoc.</li>
<li>Auto-reply/media: share one run-scoped reply media context between streamed block delivery and final payload filtering, so a local <code>MEDIA:</code> attachment is staged once and duplicate media sends are suppressed reliably. (#68111) Thanks @ayeshakhalid192007-dev.</li>
<li>Plugins/gateway hooks: expose startup config, workspace dir, and a live cron getter on the typed <code>gateway_start</code> hook, and move memory-core managed dreaming off the internal <code>gateway:startup</code> bridge so cron reconciliation stays on the public plugin hook path. Thanks @vincentkoc.</li>
<li>Plugins/config: read plugin trust decisions from the source config snapshot when a resolved runtime snapshot is active, so <code>plugins.allow</code> remains enforced and <code>doctor</code>/gateway startup no longer warn that the allowlist is empty when it is configured. Fixes #70161. Also fixes #70141.</li>
<li>Agents/openai-completions: enable malformed streamed tool-call argument repair for self-hosted OpenAI-compatible backends such as Kimi/SGLang, so fragmented tool-call arguments no longer reach tools as empty or unusable objects. Fixes #69672. (#70294) Thanks @MonkeyLeeT.</li>
<li>Gateway/restart: preserve group and channel chat context when resuming an agent turn after a Gateway restart, so continuation replies keep the same prompt, routing, and tool-status behavior as the original conversation.</li>
<li>Gateway/pairing: shared-secret loopback CLI clients now silently auto-approve <code>metadata-upgrade</code> pairing (platform / device family refresh) instead of being disconnected with <code>1008 pairing required</code>. This matches the scope-upgrade and role-upgrade behavior added in #69431 and unblocks non-interactive CLI automation when a paired-device record has a stale platform string (e.g. device key replicated across hosts, install migrated between OSes, or platform-string format changed between OpenClaw versions). Browser / Control-UI clients keep the existing approval-required flow for metadata changes.</li>
<li>Gateway/pairing: treat any forwarded-header evidence (<code>Forwarded</code>, <code>X-Forwarded-*</code>, or <code>X-Real-IP</code>) as proxied WebSocket traffic before pairing locality checks, so reverse-proxy topologies cannot use the loopback shared-secret helper auto-pairing path.</li>
<li>Agents/OpenAI: treat exact <code>NO_REPLY</code> assistant output as a deliberate silent reply in embedded runs, so GPT-5.4 turns with signed reasoning plus a silent final no longer surface a false incomplete-turn error.</li>
<li>Auto-reply/streaming: preserve streamed reply directives through chunk boundaries and phase-aware <code>final_answer</code> delivery, so split <code>MEDIA:<path></code> lines, voice tags, and reply targets reach channel delivery instead of leaking as text or being dropped. (#70243) Thanks @zqchris.</li>
<li>Anthropic/Claude Opus 4.7: normalize Opus 4.7 and <code>claude-cli</code> Opus 4.7 variants to a 1M context window in resolved runtime metadata and active-agent status/context reporting, so they no longer inherit the stale 200k fallback. Thanks @BunsDev.</li>
<li>Gateway/pairing webchat: render <code>/pair qr</code> replies as structured media instead of raw markdown text, preserve inline reply threading and silent-control handling on media replies, avoid persisting sensitive QR images into transcript history, and keep local webchat media embedding behind internal-only trust markers. (#70047) Thanks @BunsDev.</li>
<li>Codex harness: default app-server runs to unchained local execution, so OpenAI heartbeats can use network and shell tools without stalling behind native Codex approvals or the workspace-write sandbox.</li>
<li>Codex harness: fail closed for unknown native app-server approval methods instead of routing unsupported future approval shapes through OpenClaw approval grants. (#70356) Thanks @Lucenx9.</li>
<li>Codex harness: apply the GPT-5 behavior and heartbeat prompt overlay to native Codex app-server runs, so <code>codex/gpt-5.x</code> sessions get the same follow-through, tool-use, and proactive heartbeat guidance as OpenAI GPT-5 runs.</li>
<li>Codex harness: add an explicit Guardian mode for Codex app-server approvals, plus a Docker live probe for approved and ask-back Guardian decisions, while keeping default app-server runs unchained for unattended local heartbeats. The legacy <code>OPENCLAW_CODEX_APP_SERVER_GUARDIAN</code> shortcut is removed; use plugin config <code>appServer.mode: "guardian"</code> or <code>OPENCLAW_CODEX_APP_SERVER_MODE=guardian</code>. Thanks @pashpashpash.</li>
<li>OpenAI/Responses: keep embedded OpenAI Responses runs on HTTP when <code>models.providers.openai.baseUrl</code> points at a local mock or other non-public endpoint, so mocked/custom endpoints no longer drift onto the hardcoded public websocket transport. (#69815) Thanks @vincentkoc.</li>
<li>Channels/config: require resolved runtime config on channel send/action/client helpers and block runtime helper <code>loadConfig()</code> calls, so SecretRefs are resolved at startup/boundaries instead of being re-read during sends.</li>
<li>Discord: pass resolved runtime config through guild and moderation action helpers, so thread-originated Discord commands can run channel, member, role, and guild actions without falling back to runtime config reads. (#70215) Thanks @szponeczek.</li>
<li>CLI/channels: preserve bundled setup promotion metadata when a loaded partial channel plugin omits it, so adding a non-default account still moves legacy single-account fields such as Telegram <code>streaming</code> into <code>accounts.default</code>.</li>
<li>Telegram: keep the sent-message ownership cache isolated per configured session store, so own-message reaction filtering remains correct with custom <code>session.store</code> paths.</li>
<li>Security/update: fail closed when exact pinned npm plugin or hook-pack updates detect integrity drift, and expose aborted plugin drift details in <code>openclaw update --json</code>.</li>
<li>Ollama: forward OpenClaw thinking control to native <code>/api/chat</code> requests as top-level <code>think</code>, so <code>/think off</code> and <code>openclaw agent --thinking off</code> suppress thinking on models such as qwen3 instead of idling until the watchdog fires. Fixes #69902. (#69967) Thanks @WZH8898.</li>
<li>Memory-core/dreaming: suppress the startup-only managed dreaming cron unavailable warning when the cron service is still attaching, while preserving the runtime warning if cron genuinely remains unavailable. Fixes #69939. (#69941) Thanks @Sanjays2402.</li>
<li>Mattermost: suppress reasoning-only payloads even when they arrive as blockquoted <code>> Reasoning:</code> text, preventing <code>/reasoning on</code> from leaking thinking into channel posts. (#69927) Thanks @lawrence3699.</li>
<li>Discord: read <code>channel.parentId</code> through a safe accessor in the slash-command, reaction, and model-picker paths so partial <code>GuildThreadChannel</code> prototype getters no longer throw <code>Cannot access rawData on partial Channel</code> when commands like <code>/new</code> run from inside a thread. Fixes #69861. (#69908) Thanks @neeravmakwana.</li>
<li>Discord: use safe channel name and parent accessors across voice command authorization, so <code>/vc</code> commands from partial Discord thread channels no longer crash on Carbon rawData getters. (#70199) Thanks @hanamizuki.</li>
<li>Discord: make auto-thread parent transcript inheritance opt-in via <code>channels.discord.thread.inheritParent</code>, keeping newly created Discord thread sessions isolated by default while preserving explicit inheritance for configured accounts. Fixes #69907. (#69986) Thanks @Blahdude.</li>
<li>Browser/Chrome MCP: reset cached existing-session control sessions when a <code>navigate_page</code> call times out, so one stuck navigation no longer poisons the browser profile until a gateway restart. (#69733) Thanks @ayeshakhalid192007-dev.</li>
<li>Browser/Chrome MCP: propagate click timeouts and abort signals to existing-session actions so a stuck click fails fast and reconnects instead of poisoning the browser tool until gateway restart. (#63524) Thanks @dongseok0.</li>
<li>Amazon Bedrock/prompt caching: resolve opaque application inference profile targets before injecting Bedrock cache points, require every routed target to support explicit cache points, and retry transient profile lookups instead of caching a false negative for the rest of the process. (#69953) Thanks @anirudhmarc and @vincentkoc.</li>
<li>Gateway/channel health: base stale-socket recovery on provider-proven transport activity instead of inbound app-event freshness, preventing quiet Slack, Discord, Telegram, Matrix, and local-style channels from being restarted solely because no user traffic arrived. (#69833) Thanks @bek91.</li>
<li>OpenCode Go: canonicalize stale bundled <code>opencode-go</code> base URLs from <code>/go</code> or <code>/go/v1</code> to <code>/zen/go</code> or <code>/zen/go/v1</code>, so older generated model metadata stops hitting the 404 HTML endpoint. (#69898)</li>
<li>CLI/channels: honor <code>channels.<id>.enabled=false</code> as a hard read-only presence opt-out, so env vars, manifest env vars, or stale persisted auth state no longer make disabled channel plugins appear in status, doctor, or setup-only discovery.</li>
<li>Channels/preview streaming: centralize draft-preview finalization so Slack, Discord, Mattermost, and Matrix no longer flush temporary preview messages for media/error finals, and preserve first-reply threading for normal fallback delivery.</li>
<li>Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long <code>/status</code> output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras.</li>
<li>Gateway/session history: re-check current auth and <code>chat.history</code> scope before later SSE keepalives and transcript updates, so active session-history streams close before delivering post-revocation events.</li>
<li>Plugins/discovery: reject package plugin source entries that escape the package directory before explicit runtime entries or inferred built JavaScript peers can be used. (#69868) thanks @gumadeiras.</li>
<li>CLI/channels: resolve channel presence through a shared policy that keeps ambient env vars and stale persisted auth from surfacing disabled bundled plugins in status, doctor, security audit, and cron delivery validation unless the channel or plugin is effectively enabled or explicitly configured. (#69862) Thanks @gumadeiras.</li>
<li>Doctor/plugins: hydrate legacy partial interactive handler state before plugin reload clears dedupe caches, so <code>openclaw doctor</code> and post-update doctor runs no longer crash with <code>Cannot read properties of undefined (reading 'clear')</code>. (#70135) Thanks @ngutman.</li>
<li>Control UI/config: preserve intentionally empty raw config snapshots when clearing pending updates so reset restores the original bytes instead of synthesizing JSON for blank config files. (#68178) Thanks @BunsDev.</li>
<li>memory-core/dreaming: surface a <code>Dreaming status: blocked</code> line in <code>openclaw memory status</code> when dreaming is enabled but the heartbeat that drives the managed cron is not firing for the default agent, and add a Troubleshooting section to the dreaming docs covering the two common causes (per-agent <code>heartbeat</code> blocks excluding <code>main</code>, and <code>heartbeat.every</code> set to <code>0</code>/empty/invalid), so the silent failure described in #69843 becomes legible on the status surface.</li>
<li>Cron/run-log: report generic <code>message</code> tool sends under the resolved delivery channel when they match the cron target, while preserving account-specific mismatch checks for delivery traces. (#69940) Thanks @davehappyminion.</li>
<li>Doctor/channels: merge configured-channel doctor hooks across read-only, loaded, setup, and runtime plugin discovery so partial adapters no longer hide runtime-only compatibility repair or allowlist warnings, preserve disabled-channel opt-outs, and ignore malformed hook values before they can mask valid fallbacks. (#69919) Thanks @gumadeiras.</li>
<li>Models/CLI: show bundled provider-owned static catalog rows in <code>models list --all</code> before auth is configured, including Kimi K2.6 rows for Moonshot, OpenRouter, and Vercel AI Gateway, while keeping local-only and workspace plugin catalog paths isolated. (#69909) Thanks @shakkernerd.</li>
<li>Models/CLI: clarify that <code>models list --provider</code> expects provider ids and reject display labels before loading model discovery. (#70504) Thanks @shakkernerd.</li>
<li>Configure: skip generic CLI startup bootstrap for <code>openclaw configure</code> and bound hint-only gateway probes so the onboarding TUI reaches its first prompt faster when the Gateway is unavailable. (#69984) Thanks @obviyus.</li>
<li>Agents/harness: surface selected plugin harness failures directly instead of replaying the same turn through embedded PI, preventing misleading secondary PI auth errors and avoiding duplicate side effects.</li>
<li>OpenAI Codex: add a ChatGPT device-code auth option beside browser OAuth, so headless or callback-hostile setups can sign in without relying on the localhost browser callback. (#69557) Thanks @vincentkoc.</li>
<li>CLI sessions: keep provider-owned CLI sessions through implicit daily expiry while preserving explicit reset behavior, and retain Claude CLI binding metadata across gateway agent requests. (#70106) Thanks @obviyus.</li>
<li>fix(config): accept truncateAfterCompaction (#68395). Thanks @MonkeyLeeT</li>
<li>CLI/Claude: keep Claude CLI session bindings stable across OAuth access-token refreshes, so gateway restarts continue the same Claude conversation instead of minting a fresh one. (#70132) Thanks @obviyus.</li>
<li>QQBot: add <code>INTERACTION</code> intent (<code>1 << 26</code>) to the gateway constants and include it in the <code>FULL_INTENTS</code> mask so interaction events are received. (#70143) Thanks @cxyhhhhh.</li>
<li>Gateway/restart: preserve one-shot continuation instructions across gateway restarts so agents can resume and reply back to the original chat after reboot. (#63406) Thanks @VACInc.</li>
<li>Gateway/restart: write restart sentinel files atomically so interrupted writes cannot leave a truncated sentinel behind. (#70225) Thanks @obviyus.</li>
<li>Pairing: remove stale pending requests for a device when that paired device is deleted, so an old repair approval cannot recreate the removed device from leftover state.</li>
<li>Security/dotenv: block workspace <code>.env</code> overrides for Matrix, Mattermost, IRC, and Synology endpoint settings so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. (#70240) Thanks @drobison00.</li>
<li>Telegram: require the same <code>/models</code> authorization for group model-picker callbacks, so unauthorized participants can no longer browse or change the session model through inline buttons. (#70235) Thanks @drobison00.</li>
<li>Agents/Pi: keep the filtered tool-name allowlist active for embedded OpenAI/OpenAI Codex GPT-5 runs and compaction sessions, so bundled and client tools still execute after the Pi <code>0.68.1</code> session-tool allowlist change instead of stopping at plan-only replies with no tool call. (#70281) Thanks @jalehman.</li>
<li>Agents/Pi: honor explicit <code>strict-agentic</code> execution contracts for incomplete-turn retry guards across providers, so manually opted-in local or compatible models get the same retry behavior without relying on OpenAI model inference. (#66750) Thanks @ziomancer.</li>
<li>OpenShell/sandbox: pin verified file reads to an already-opened descriptor, walk the ancestor chain for symlinked parents on platforms without fd-path readlink, and re-check file identity so parent symlink swaps cannot redirect in-sandbox reads to host files outside the allowed mount root. (#69798) Thanks @drobison00.</li>
<li>Gateway/Control UI: require authenticated Control UI read access before serving <code>/__openclaw/control-ui-config.json</code> when <code>gateway.auth</code> is enabled, so unauthenticated callers can no longer read bootstrap metadata. (#70247) Thanks @drobison00.</li>
<li>Gateway/restart: default session-scoped restart sentinels to a one-shot agent continuation, so chat-initiated Gateway restarts acknowledge successful boot automatically. (#70269) Thanks @obviyus.</li>
<li>Build/npm publish: fail postpublish verification when root <code>dist/*</code> files import bundled plugin runtime dependencies without mirroring them in the root package manifest, so Slack-style plugin deps cannot silently ship on the wrong module-resolution path again. (#60112) thanks @medns.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.22/OpenClaw-2026.4.22.zip" length="47883836" type="application/octet-stream" sparkle:edSignature="kzJ2j2sWX4H+ZIc4dXEFORYr9tk3w1txpjCJ38cdSFz6yWHU0M6Sx9zN0DB7JGIpv1QC+D+jFbWBkl4SJqW2AA=="/>
</item>
<item>
<title>2026.4.20</title>
<pubDate>Tue, 21 Apr 2026 19:53:52 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042090</sparkle:version>
<sparkle:shortVersionString>2026.4.20</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.20</h2>
<h3>Changes</h3>
<ul>
<li>Onboard/wizard: restyle the setup security disclaimer with a single yellow warning banner, section headings and bulleted checklists, and un-dim the note body so key guidance is easy to scan; add a loading spinner during the initial model catalog load so the wizard no longer goes blank while it runs; add an "API key" placeholder to provider API key prompts. (#69553) Thanks @Patrick-Erichsen.</li>
<li>Agents/prompts: strengthen the default system prompt and OpenAI GPT-5 overlay with clearer completion bias, live-state checks, weak-result recovery, and verification-before-final guidance.</li>
<li>Models/costs: support tiered model pricing from cached catalogs and configured models, and include bundled Moonshot Kimi K2.6/K2.5 cost estimates for token-usage reports. (#67605) Thanks @sliverp.</li>
<li>Sessions/Maintenance: enforce the built-in entry cap and age prune by default, and prune oversized stores at load time so accumulated cron/executor session backlogs cannot OOM the gateway before the write path runs. (#69404) Thanks @bobrenze-bot.</li>
<li>Plugins/tests: reuse plugin loader alias and Jiti config resolution across repeated same-context loads, reducing import-heavy test overhead. (#69316) Thanks @amknight.</li>
<li>Cron: split runtime execution state into <code>jobs-state.json</code> so <code>jobs.json</code> stays stable for git-tracked job definitions. (#63105) Thanks @Feelw00.</li>
<li>Agents/compaction: send opt-in start and completion notices during context compaction. (#67830) Thanks @feniix.</li>
<li>Moonshot/Kimi: default bundled Moonshot setup, web search, and media-understanding surfaces to <code>kimi-k2.6</code> while keeping <code>kimi-k2.5</code> available for compatibility. (#69477) Thanks @scoootscooob.</li>
<li>Moonshot/Kimi: allow <code>thinking.keep = "all"</code> on <code>moonshot/kimi-k2.6</code>, and strip it for other Moonshot models or requests where pinned <code>tool_choice</code> disables thinking. (#68816) Thanks @aniaan.</li>
<li>BlueBubbles/groups: forward per-group <code>systemPrompt</code> config into inbound context <code>GroupSystemPrompt</code> so configured group-specific behavioral instructions (for example threaded-reply and tapback conventions) are injected on every turn. Supports <code>"*"</code> wildcard fallback matching the existing <code>requireMention</code> pattern. Closes #60665. (#69198) Thanks @omarshahine.</li>
<li>Plugins/tasks: add a detached runtime registration contract so plugin executors can own detached task lifecycle and cancellation without reaching into core task internals. (#68915) Thanks @mbelinky.</li>
<li>Terminal/logging: optimize <code>sanitizeForLog()</code> by replacing the iterative control-character stripping loop with a single regex pass while preserving the existing ANSI-first sanitization behavior. (#67205) Thanks @bulutmuf.</li>
<li>QA/CI: make <code>openclaw qa suite</code> and <code>openclaw qa telegram</code> fail by default when scenarios fail, add <code>--allow-failures</code> for artifact-only runs, and tighten live-lane defaults for CI automation. (#69122) Thanks @joshavant.</li>
<li>Mattermost: stream thinking, tool activity, and partial reply text into a single draft preview post that finalizes in place when safe. (#47838) thanks @ninjaa.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Exec/YOLO: stop rejecting gateway-host exec in <code>security=full</code> plus <code>ask=off</code> mode via the Python/Node script preflight hardening path, so promptless YOLO exec once again runs direct interpreter stdin and heredoc forms such as <code>node <<'NODE' ... NODE</code>.</li>
<li>OpenAI Codex: normalize legacy <code>openai-completions</code> transport overrides on default OpenAI/Codex and GitHub Copilot-compatible hosts back to the native Codex Responses transport while leaving custom proxies untouched. (#45304, #42194) Thanks @dyss1992 and @DeadlySilent.</li>
<li>Anthropic/plugins: scope Anthropic <code>api: "anthropic-messages"</code> defaulting to Anthropic-owned providers, so <code>openai-codex</code> and other providers without an explicit <code>api</code> no longer get rewritten to the wrong transport. Fixes #64534.</li>
<li>fix(qqbot): add SSRF guard to direct-upload URL paths in uploadC2CMedia and uploadGroupMedia [AI-assisted]. (#69595) Thanks @pgondhi987.</li>
<li>fix(gateway): enforce allowRequestSessionKey gate on template-rendered mapping sessionKeys. (#69381) Thanks @pgondhi987.</li>
<li>Browser/Chrome MCP: surface <code>DevToolsActivePort</code> attach failures as browser-connectivity errors instead of a generic "waiting for tabs" timeout, and point signed-out fallbacks toward the managed <code>openclaw</code> profile.</li>
<li>Webchat/images: treat inline image attachments as media for empty-turn gating while still ignoring metadata-only blank turns. (#69474) Thanks @Jaswir.</li>
<li>Discord/think: only show <code>adaptive</code> in <code>/think</code> autocomplete for provider/model pairs that actually support provider-managed adaptive thinking, so GPT/OpenAI models no longer advertise an Anthropic-only option.</li>
<li>Thinking: only expose <code>max</code> for models that explicitly support provider max reasoning, and remap stored <code>max</code> settings to the largest supported thinking mode when users switch to another model.</li>
<li>Gateway/usage: bound the cost usage cache with FIFO eviction so date/range lookups cannot grow unbounded. (#68842) Thanks @Feelw00.</li>
<li>OpenAI/Responses: resolve <code>/think</code> levels against each GPT model's supported reasoning efforts so <code>/think off</code> no longer becomes high reasoning or sends unsupported <code>reasoning.effort: "none"</code> payloads.</li>
<li>Lobster/TaskFlow: allow managed approval resumes to use <code>approvalId</code> without a resume token, and persist that id in approval wait state. (#69559) Thanks @kirkluokun.</li>
<li>Plugins/startup: install bundled runtime dependencies into each plugin's own runtime directory, reuse source-checkout repair caches after rebuilds, and log only packages that were actually installed so repeated Gateway starts stay quiet once deps are present.</li>
<li>Plugins/startup: ignore pnpm's <code>npm_execpath</code> when repairing bundled plugin runtime dependencies and skip workspace-only package specs so npm-only install flags or local workspace links do not break packaged plugin startup.</li>
<li>MCP: block interpreter-startup env keys such as <code>NODE_OPTIONS</code> for stdio servers while preserving ordinary credential and proxy env vars. (#69540) Thanks @drobison00.</li>
<li>Agents/shell: ignore non-interactive placeholder shells like <code>/usr/bin/false</code> and <code>/sbin/nologin</code>, falling back to <code>sh</code> so service-user exec runs no longer exit immediately. (#69308) Thanks @sk7n4k3d.</li>
<li>Setup/TUI: relaunch the setup hatch TUI in a fresh process while preserving the configured gateway target and auth source, so onboarding recovers terminal state cleanly without exposing gateway secrets on command-line args. (#69524) Thanks @shakkernerd.</li>
<li>Codex: avoid re-exposing the image-generation tool on native vision turns with inbound images, and keep bare image-model overrides on the configured image provider. (#65061) Thanks @zhulijin1991.</li>
<li>Sessions/reset: clear auto-sourced model, provider, and auth-profile overrides on <code>/new</code> and <code>/reset</code> while preserving explicit user selections, so channel sessions stop staying pinned to runtime fallback choices. (#69419) Thanks @sk7n4k3d.</li>
<li>Sessions/costs: snapshot <code>estimatedCostUsd</code> like token counters so repeated persist paths no longer compound the same run cost by up to dozens of times. (#69403) Thanks @MrMiaigi.</li>
<li>OpenAI Codex: route ChatGPT/Codex OAuth Responses requests through the <code>/backend-api/codex</code> endpoint so <code>openai-codex/gpt-5.4</code> no longer hits the removed <code>/backend-api/responses</code> alias. (#69336) Thanks @mzogithub.</li>
<li>OpenAI/Responses: omit disabled reasoning payloads when <code>/think off</code> is active, so GPT reasoning models no longer receive unsupported <code>reasoning.effort: "none"</code> requests. (#61982) Thanks @a-tokyo.</li>
<li>Gateway/pairing: treat loopback shared-secret node-host, TUI, and gateway clients as local for pairing decisions, so trusted local tools no longer reconnect as remote clients and fail with <code>pairing required</code>. (#69431) Thanks @SARAMALI15792.</li>
<li>Active Memory: degrade gracefully when memory recall fails during prompt building, logging a warning and letting the reply continue without memory context instead of failing the whole turn. (#69485) Thanks @Magicray1217.</li>
<li>Ollama: add provider-policy defaults for <code>baseUrl</code> and <code>models</code> so implicit local discovery can run before config validation rejects a minimal Ollama provider config. (#69370) Thanks @PratikRai0101.</li>
<li>Agents/model selection: clear transient auto-failover session overrides before each turn so recovered primary models are retried immediately without emitting user-override reset warnings. (#69365) Thanks @hitesh-github99.</li>
<li>Auto-reply: apply silent <code>NO_REPLY</code> policy per conversation type, so direct chats get a helpful rewritten reply while groups and internal deliveries can remain quiet. (#68644) Thanks @Takhoffman.</li>
<li>Telegram/status reactions: honor <code>messages.removeAckAfterReply</code> when lifecycle status reactions are enabled, clearing or restoring the reaction after success/error using the configured hold timings. (#68067) Thanks @poiskgit.</li>
<li>Web search/plugins: resolve plugin-scoped SecretRef API keys for bundled Exa, Firecrawl, Gemini, Kimi, Perplexity, Tavily, and Grok web-search providers when they are selected through the shared web-search config. (#68424) Thanks @afurm.</li>
<li>Telegram/polling: raise the default polling watchdog threshold from 90s to 120s and add configurable <code>channels.telegram.pollingStallThresholdMs</code> (also per-account) so long-running Telegram work gets more room before polling is treated as stalled. (#57737) Thanks @Vitalcheffe.</li>
<li>Telegram/polling: bound the persisted-offset confirmation <code>getUpdates</code> probe with a client-side timeout so a zombie socket cannot hang polling recovery before the runner watchdog starts. (#50368) Thanks @boticlaw.</li>
<li>Agents/Pi runner: retry silent <code>stopReason=error</code> turns with no output when no side effects ran, so non-frontier providers that briefly return empty error turns get another chance instead of ending the session early. (#68310) Thanks @Chased1k.</li>
<li>Plugins/memory: preserve the active memory capability when read-only snapshot plugin loads run, so status and provider discovery paths no longer wipe memory public artifacts. (#69219) Thanks @zeroaltitude.</li>
<li>Plugins: keep only the highest-precedence manifest when distinct discovered plugins share an id, so lower-precedence global or workspace duplicates no longer load beside bundled or config-selected plugins. (#41626) Thanks @Tortes.</li>
<li>fix(security): block MINIMAX_API_HOST workspace env injection and remove env-driven URL routing [AI-assisted]. (#67300) Thanks @pgondhi987.</li>
<li>Cron/delivery: treat explicit <code>delivery.mode: "none"</code> runs as not requested even if the runner reports <code>delivered: false</code>, so no-delivery cron jobs no longer persist false delivery failures or errors. (#69285) Thanks @matsuri1987.</li>
<li>Plugins/install: repair active and default-enabled bundled plugin runtime dependencies before import in packaged installs, so bundled Discord, WhatsApp, Slack, Telegram, and provider plugins work without putting their dependency trees in core.</li>
<li>BlueBubbles: raise the outbound <code>/api/v1/message/text</code> send timeout default from 10s to 30s, and add a configurable <code>channels.bluebubbles.sendTimeoutMs</code> (also per-account) so macOS 26 setups where Private API iMessage sends stall for 60+ seconds no longer silently lose messages at the 10s abort. Probes, chat lookups, and health checks keep the shorter 10s default. Fixes #67486. (#69193) Thanks @omarshahine.</li>
<li>Agents/bootstrap: budget truncation markers against per-file caps, preserve source content instead of silently wasting bootstrap bytes, and avoid marker-only output in tiny-budget truncation cases. (#69114) Thanks @BKF-Gitty.</li>
<li>Context engine/plugins: stop rejecting third-party context engines whose <code>info.id</code> differs from the registered plugin slot id. The strict-match contract added in 2026.4.14 broke <code>lossless-claw</code> and other plugins whose internal engine id does not equal the slot id they are registered under, producing repeated <code>info.id must match registered id</code> lane failures on every turn. Fixes #66601. (#66678) Thanks @GodsBoy.</li>
<li>Agents/compaction: rename embedded Pi compaction lifecycle events to <code>compaction_start</code> / <code>compaction_end</code> so OpenClaw stays aligned with <code>pi-coding-agent</code> 0.66.1 event naming. (#67713) Thanks @mpz4life.</li>
<li>Security/dotenv: block all <code>OPENCLAW_*</code> keys from untrusted workspace <code>.env</code> files so workspace-local env loading fails closed for new runtime-control variables instead of silently inheriting them. (#473)</li>
<li>Gateway/device pairing: restrict non-admin paired-device sessions (device-token auth) to their own pairing list, approve, and reject actions so a paired device cannot enumerate other devices or approve/reject pairing requests authored by another device. Admin and shared-secret operator sessions retain full visibility. (#69375) Thanks @eleqtrizit.</li>
<li>Agents/gateway tool: extend the agent-facing <code>gateway</code> tool's config mutation guard so model-driven <code>config.patch</code> and <code>config.apply</code> cannot rewrite operator-trusted paths (sandbox, plugin trust, gateway auth/TLS, hook routing and tokens, SSRF policy, MCP servers, workspace filesystem hardening) and cannot bypass the guard by editing per-agent sandbox, tools, or embedded-Pi overrides in place under <code>agents.list[]</code>. (#69377) Thanks @eleqtrizit.</li>
<li>Gateway/websocket broadcasts: require <code>operator.read</code> (or higher) for chat, agent, and tool-result event frames so pairing-scoped and node-role sessions no longer passively receive session chat content, and scope-gate unknown broadcast events by default. Plugin-defined <code>plugin.*</code> broadcasts are scoped to operator.write/admin, and status/transport events (<code>heartbeat</code>, <code>presence</code>, <code>tick</code>, etc.) remain unrestricted. Per-client sequence numbers preserve per-connection monotonicity. (#69373) Thanks @eleqtrizit.</li>
<li>Agents/compaction: always reload embedded Pi resources through an explicit loader and reapply reserve-token overrides so runs without extension factories no longer silently lose compaction settings before session start. (#67146) Thanks @ly85206559.</li>
<li>Memory-core/dreaming: normalize sweep timestamps and reuse hashed narrative session keys for fallback cleanup so Dreaming narrative sub-sessions stop leaking. (#67023) Thanks @chiyouYCH.</li>
<li>Gateway/startup: delay HTTP bind until websocket handlers are attached, so immediate post-startup websocket health/connect probes no longer hit the startup race window. (#43392) Thanks @dalefrieswthat.</li>
<li>Codex/app-server: release the session lane when a downstream consumer throws while draining the <code>turn/completed</code> notification, so follow-up messages after a Codex plugin reply stop queueing behind a stale lane lock. Fixes #67996. (#69072) Thanks @ayeshakhalid192007-dev.</li>
<li>Codex/app-server: default approval handling to <code>on-request</code> so Codex harness sessions do not start with overly permissive tool approvals. (#68721) Thanks @Lucenx9.</li>
<li>Cron/delivery: keep isolated cron chat delivery tools available, resolve <code>channel: "last"</code> targets from the gateway, show delivery previews in <code>cron list/show</code>, and avoid duplicate fallback sends after direct message-tool delivery. (#69587) Thanks @obviyus.</li>
<li>Cron/Telegram: key isolated direct-delivery dedupe to each cron execution instead of the reused session id, so recurring Telegram announce runs no longer report delivered while silently skipping later sends. (#69000) Thanks @obviyus.</li>
<li>Models/Kimi: default bundled Kimi thinking to off and normalize Anthropic-compatible <code>thinking</code> payloads so stale session <code>/think</code> state no longer silently re-enables reasoning on Kimi runs. (#68907) Thanks @frankekn.</li>
<li>Control UI/cron: keep the runtime-only <code>last</code> delivery sentinel from being materialized into persisted cron delivery and failure-alert channel configs when jobs are created or edited. (#68829) Thanks @tianhaocui.</li>
<li>OpenAI/Responses: strip orphaned reasoning blocks before outbound Responses API calls so compacted or restored histories no longer fail on standalone reasoning items. (#55787) Thanks @suboss87.</li>
<li>Cron/CLI: parse PowerShell-style <code>--tools</code> allow-lists the same way as comma-separated input, so <code>cron add</code> and <code>cron edit</code> no longer persist <code>exec read write</code> as one combined tool entry on Windows. (#68858) Thanks @chen-zhang-cs-code.</li>
<li>Browser/user-profile: let existing-session <code>profile="user"</code> tool calls auto-route to a connected browser node or use explicit <code>target="node"</code>, while still honoring explicit <code>target="host"</code> pinning. (#48677)</li>
<li>Discord/slash commands: tolerate partial Discord channel metadata in slash-command and model-picker flows so partial channel objects no longer crash when channel names, topics, or thread parent metadata are unavailable. (#68953) Thanks @dutifulbob.</li>
<li>BlueBubbles: consolidate outbound HTTP through a typed <code>BlueBubblesClient</code> that resolves the SSRF policy once at construction so image attachments stop getting blocked on localhost and reactions stop getting blocked on private-IP BB deployments. Fixes #34749 and #59722. (#68234) Thanks @omarshahine.</li>
<li>Cron/gateway: reject ambiguous announce delivery config at add/update time so invalid multi-channel or target-id provider settings fail early instead of persisting broken cron jobs. (#69015) Thanks @obviyus.</li>
<li>Cron/main-session delivery: preserve <code>heartbeat.target="last"</code> through deferred wake queuing, gateway wake forwarding, and same-target wake coalescing so queued cron replies still return to the last active chat. (#69021) Thanks @obviyus.</li>
<li>Cron/gateway: ignore disabled channels when announce delivery ambiguity is checked, and validate main-session delivery patches against the live cron service default agent so hot-reloaded agent config does not falsely reject valid updates. (#69040) Thanks @obviyus.</li>
<li>Matrix/allowlists: hot-reload <code>dm.allowFrom</code> and <code>groupAllowFrom</code> entries on inbound messages while keeping config removals authoritative, so Matrix allowlist changes no longer require a channel restart to add or revoke a sender. (#68546) Thanks @johnlanni.</li>
<li>BlueBubbles: always set <code>method</code> explicitly on outbound text sends (<code>"private-api"</code> when available, <code>"apple-script"</code> otherwise), and prefer Private API on macOS 26 even for plain text. Fixes silent delivery failure on macOS setups without Private API where an omitted <code>method</code> let BB Server fall back to version-dependent default behavior that silently drops the message (#64480), and the AppleScript <code>-1700</code> error on macOS 26 Tahoe plain text sends (#53159). (#69070) Thanks @xqing3.</li>
<li>Matrix/commands: recognize slash commands that are prefixed with the bot's Matrix mention, so room messages like <code>@bot:server /new</code> trigger the command path without requiring custom mention regexes. (#68570) Thanks @nightq and @johnlanni.</li>
<li>Gateway/pairing: return reason-specific <code>PAIRING_REQUIRED</code> details, remediation hints, and request ids so unapproved-device and scope-upgrade failures surface actionable recovery guidance in the CLI and Control UI. (#69227) Thanks @obviyus.</li>
<li>Agents/subagents: include requested role and runtime timing on subagent failure payloads so parent agents can correlate failed or timed-out child work. (#68726) Thanks @BKF-Gitty.</li>
<li>Gateway/sessions: reject stale agent-scoped sessions after an agent is removed from config while preserving legacy default-agent main-session aliases. (#65986) Thanks @bittoby.</li>
<li>Doctor/gateway: surface pending device pairing requests, scope-upgrade approval drift, and stale device-token mismatch repair steps so <code>openclaw doctor --fix</code> no longer leaves pairing/auth setup failures unexplained. (#69210) Thanks @obviyus.</li>
<li>Cron/isolated-agent: preserve explicit <code>delivery.mode: "none"</code> message targets for isolated runs without inheriting implicit <code>last</code> routing, so agent-initiated Telegram sends keep their authored destination while bare <code>mode:none</code> jobs stay targetless. (#69153) Thanks @obviyus.</li>
<li>Cron/isolated-agent: keep <code>delivery.mode: "none"</code> 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 <code>to</code> target. (#69163) Thanks @obviyus.</li>
<li>Gateway/TUI: retry session history while the local gateway is still finishing startup, so <code>openclaw tui</code> reconnects no longer fail on transient <code>chat.history unavailable during gateway startup</code> errors. (#69164) Thanks @shakkernerd.</li>
<li>BlueBubbles/reactions: fall back to <code>love</code> when an agent reacts with an emoji outside the iMessage tapback set (<code>love</code>/<code>like</code>/<code>dislike</code>/<code>laugh</code>/<code>emphasize</code>/<code>question</code>), so wider-vocabulary model reactions like <code>👀</code> still produce a visible tapback instead of failing the whole reaction request. Configured ack reactions still validate strictly via the new <code>normalizeBlueBubblesReactionInputStrict</code> path. (#64693) Thanks @zqchris.</li>
<li>BlueBubbles: prefer iMessage over SMS when both chats exist for the same handle, honor explicit <code>sms:</code> targets, and never silently downgrade iMessage-available recipients. (#61781) Thanks @rmartin.</li>
<li>Telegram/setup: require numeric <code>allowFrom</code> user IDs during setup instead of offering unsupported <code>@username</code> DM resolution, and point operators to <code>from.id</code>/<code>getUpdates</code> for discovery. (#69191) Thanks @obviyus.</li>
<li>GitHub Copilot/onboarding: default GitHub Copilot setup to <code>claude-opus-4.6</code> and keep the bundled default model list aligned, so new Copilot setups no longer start on the older <code>gpt-4o</code> default. (#69207) Thanks @obviyus.</li>
<li>Gateway/status: separate reachability, capability, and read-probe reporting so connect-only or scope-limited sessions no longer look fully healthy, and normalize SSH targets entered as <code>ssh user@host</code>. (#69215) Thanks @obviyus.</li>
<li>Slack: fix outbound replies failing with "unresolved SecretRef" for accounts configured via <code>file</code> or <code>exec</code> secret sources; the send path now tolerates the runtime snapshot retaining an unresolved channel SecretRef when a boot-resolved token override is already available. (#68954) Thanks @openperf.</li>
<li>Control UI/device pairing: explain scope and role approval upgrades during reconnects, and show requested versus approved access in the Control UI and <code>openclaw devices</code> so broader reconnects no longer look like lost pairings. (#69221) Thanks @obviyus.</li>
<li>Gateway/Control UI: surface pending scope, role, and device-metadata pairing approvals in auth errors and Control UI hints so broader reconnects no longer look like random auth breakage. (#69226) Thanks @obviyus.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.20/OpenClaw-2026.4.20.zip" length="47535600" type="application/octet-stream" sparkle:edSignature="D7XcNGxmc10IIayYY91RZBoascFSnXyd4dg6cSpC3+PTIwVrWYs/FwSBc/1J+1P53LlnTHKDGQYMkWVNMnRSAQ=="/>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,19 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{kt,kts}]
indent_style = space
indent_size = 2
max_line_length = off
ktlint_standard_filename = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-naming = disabled
ktlint_standard_if-else-bracing = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_property-naming = disabled

View File

@@ -15,6 +15,7 @@ Status: **extremely alpha**. The app is actively being rebuilt from the ground u
- [x] Request camera/location and other permissions in onboarding/settings flow
- [x] Push notifications for gateway/chat status updates
- [x] Security hardening (biometric lock, token handling, safer defaults)
- [x] Authenticated background presence beacons
- [x] Voice tab full functionality
- [x] Screen tab full functionality
- [ ] Full end-to-end QA and release hardening

View File

@@ -7,284 +7,286 @@ val androidStorePassword = providers.gradleProperty("OPENCLAW_ANDROID_STORE_PASS
val androidKeyAlias = providers.gradleProperty("OPENCLAW_ANDROID_KEY_ALIAS").orNull?.takeIf { it.isNotBlank() }
val androidKeyPassword = providers.gradleProperty("OPENCLAW_ANDROID_KEY_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val resolvedAndroidStoreFile =
androidStoreFile?.let { storeFilePath ->
if (storeFilePath.startsWith("~/")) {
"${System.getProperty("user.home")}/${storeFilePath.removePrefix("~/")}"
} else {
storeFilePath
}
androidStoreFile?.let { storeFilePath ->
if (storeFilePath.startsWith("~/")) {
"${System.getProperty("user.home")}/${storeFilePath.removePrefix("~/")}"
} else {
storeFilePath
}
}
val hasAndroidReleaseSigning =
listOf(resolvedAndroidStoreFile, androidStorePassword, androidKeyAlias, androidKeyPassword).all { it != null }
listOf(resolvedAndroidStoreFile, androidStorePassword, androidKeyAlias, androidKeyPassword).all { it != null }
val wantsAndroidReleaseBuild =
gradle.startParameter.taskNames.any { taskName ->
taskName.contains("Release", ignoreCase = true) ||
Regex("""(^|:)(bundle|assemble)$""").containsMatchIn(taskName)
}
gradle.startParameter.taskNames.any { taskName ->
taskName.contains("Release", ignoreCase = true) ||
Regex("""(^|:)(bundle|assemble)$""").containsMatchIn(taskName)
}
if (wantsAndroidReleaseBuild && !hasAndroidReleaseSigning) {
error(
"Missing Android release signing properties. Set OPENCLAW_ANDROID_STORE_FILE, " +
"OPENCLAW_ANDROID_STORE_PASSWORD, OPENCLAW_ANDROID_KEY_ALIAS, and " +
"OPENCLAW_ANDROID_KEY_PASSWORD in ~/.gradle/gradle.properties.",
)
error(
"Missing Android release signing properties. Set OPENCLAW_ANDROID_STORE_FILE, " +
"OPENCLAW_ANDROID_STORE_PASSWORD, OPENCLAW_ANDROID_KEY_ALIAS, and " +
"OPENCLAW_ANDROID_KEY_PASSWORD in ~/.gradle/gradle.properties.",
)
}
plugins {
id("com.android.application")
id("org.jlleitschuh.gradle.ktlint")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.android.application")
id("org.jlleitschuh.gradle.ktlint")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
}
android {
namespace = "ai.openclaw.app"
compileSdk = 36
namespace = "ai.openclaw.app"
compileSdk = 36
// Release signing is local-only; keep the keystore path and passwords out of the repo.
signingConfigs {
if (hasAndroidReleaseSigning) {
create("release") {
storeFile = project.file(checkNotNull(resolvedAndroidStoreFile))
storePassword = checkNotNull(androidStorePassword)
keyAlias = checkNotNull(androidKeyAlias)
keyPassword = checkNotNull(androidKeyPassword)
}
}
// Release signing is local-only; keep the keystore path and passwords out of the repo.
signingConfigs {
if (hasAndroidReleaseSigning) {
create("release") {
storeFile = project.file(checkNotNull(resolvedAndroidStoreFile))
storePassword = checkNotNull(androidStorePassword)
keyAlias = checkNotNull(androidKeyAlias)
keyPassword = checkNotNull(androidKeyPassword)
}
}
}
sourceSets {
getByName("main") {
assets.directories.add("../../shared/OpenClawKit/Sources/OpenClawKit/Resources")
}
sourceSets {
getByName("main") {
assets.directories.add("../../shared/OpenClawKit/Sources/OpenClawKit/Resources")
}
}
defaultConfig {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026042500
versionName = "2026.4.25"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
defaultConfig {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026042700
versionName = "2026.4.27"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
flavorDimensions += "store"
flavorDimensions += "store"
productFlavors {
create("play") {
dimension = "store"
buildConfigField("boolean", "OPENCLAW_ENABLE_SMS", "false")
buildConfigField("boolean", "OPENCLAW_ENABLE_CALL_LOG", "false")
}
create("thirdParty") {
dimension = "store"
buildConfigField("boolean", "OPENCLAW_ENABLE_SMS", "true")
buildConfigField("boolean", "OPENCLAW_ENABLE_CALL_LOG", "true")
}
productFlavors {
create("play") {
dimension = "store"
buildConfigField("boolean", "OPENCLAW_ENABLE_SMS", "false")
buildConfigField("boolean", "OPENCLAW_ENABLE_CALL_LOG", "false")
}
buildTypes {
release {
if (hasAndroidReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
isMinifyEnabled = false
}
create("thirdParty") {
dimension = "store"
buildConfigField("boolean", "OPENCLAW_ENABLE_SMS", "true")
buildConfigField("boolean", "OPENCLAW_ENABLE_CALL_LOG", "true")
}
}
buildFeatures {
compose = true
buildConfig = true
buildTypes {
release {
if (hasAndroidReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/*.version",
"/META-INF/LICENSE*.txt",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties",
"org/bouncycastle/x509/CertPathReviewerMessages*.properties",
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
lint {
disable +=
setOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"IconLauncherShape",
"NewerVersionAvailable",
)
warningsAsErrors = true
packaging {
resources {
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/*.version",
"/META-INF/LICENSE*.txt",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties",
"org/bouncycastle/x509/CertPathReviewerMessages*.properties",
)
}
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
lint {
disable +=
setOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"HighAppVersionCode",
"IconLauncherShape",
"NewerVersionAvailable",
"OldTargetApi",
)
warningsAsErrors = true
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
androidComponents {
onVariants { variant ->
variant.outputs
.filterIsInstance<VariantOutputImpl>()
.forEach { output ->
val versionName = output.versionName.orNull ?: "0"
val buildType = variant.buildType
val flavorName = variant.flavorName?.takeIf { it.isNotBlank() }
val outputFileName =
if (flavorName == null) {
"openclaw-$versionName-$buildType.apk"
} else {
"openclaw-$versionName-$flavorName-$buildType.apk"
}
output.outputFileName = outputFileName
}
}
onVariants { variant ->
variant.outputs
.filterIsInstance<VariantOutputImpl>()
.forEach { output ->
val versionName = output.versionName.orNull ?: "0"
val buildType = variant.buildType
val flavorName = variant.flavorName?.takeIf { it.isNotBlank() }
val outputFileName =
if (flavorName == null) {
"openclaw-$versionName-$buildType.apk"
} else {
"openclaw-$versionName-$flavorName-$buildType.apk"
}
output.outputFileName = outputFileName
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
allWarningsAsErrors.set(true)
}
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
allWarningsAsErrors.set(true)
}
}
ktlint {
android.set(true)
ignoreFailures.set(false)
filter {
exclude("**/build/**")
}
android.set(true)
ignoreFailures.set(false)
filter {
exclude("**/build/**")
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.03.01")
implementation(composeBom)
androidTestImplementation(composeBom)
val composeBom = platform("androidx.compose:compose-bom:2026.04.01")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.13.0")
implementation("androidx.webkit:webkit:1.15.0")
implementation("androidx.core:core-ktx:1.18.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.13.0")
implementation("androidx.webkit:webkit:1.15.0")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// material-icons-extended pulled in full icon set (~20 MB DEX). Only ~18 icons used.
// R8 will tree-shake unused icons when minify is enabled on release builds.
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// material-icons-extended pulled in full icon set (~20 MB DEX). Only ~18 icons used.
// R8 will tree-shake unused icons when minify is enabled on release builds.
implementation("androidx.compose.material:material-icons-extended")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-tooling")
// Material Components (XML theme + resources)
implementation("com.google.android.material:material:1.13.0")
// Material Components (XML theme + resources)
implementation("com.google.android.material:material:1.13.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.exifinterface:exifinterface:1.4.2")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("org.bouncycastle:bcprov-jdk18on:1.84")
implementation("org.commonmark:commonmark:0.28.0")
implementation("org.commonmark:commonmark-ext-autolink:0.28.0")
implementation("org.commonmark:commonmark-ext-gfm-strikethrough:0.28.0")
implementation("org.commonmark:commonmark-ext-gfm-tables:0.28.0")
implementation("org.commonmark:commonmark-ext-task-list-items:0.28.0")
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.exifinterface:exifinterface:1.4.2")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("org.bouncycastle:bcprov-jdk18on:1.84")
implementation("org.commonmark:commonmark:0.28.0")
implementation("org.commonmark:commonmark-ext-autolink:0.28.0")
implementation("org.commonmark:commonmark-ext-gfm-strikethrough:0.28.0")
implementation("org.commonmark:commonmark-ext-gfm-tables:0.28.0")
implementation("org.commonmark:commonmark-ext-task-list-items:0.28.0")
// CameraX (for node.invoke camera.* parity)
implementation("androidx.camera:camera-core:1.5.2")
implementation("androidx.camera:camera-camera2:1.5.2")
implementation("androidx.camera:camera-lifecycle:1.5.2")
implementation("androidx.camera:camera-video:1.5.2")
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
// CameraX (for node.invoke camera.* parity)
implementation("androidx.camera:camera-core:1.6.0")
implementation("androidx.camera:camera-camera2:1.6.0")
implementation("androidx.camera:camera-lifecycle:1.6.0")
implementation("androidx.camera:camera-video:1.6.0")
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
implementation("dnsjava:dnsjava:3.6.4")
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
implementation("dnsjava:dnsjava:3.6.4")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.1.11")
testImplementation("io.kotest:kotest-assertions-core-jvm:6.1.11")
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
testImplementation("org.robolectric:robolectric:4.16.1")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.3")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.1.11")
testImplementation("io.kotest:kotest-assertions-core-jvm:6.1.11")
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
testImplementation("org.robolectric:robolectric:4.16.1")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.3")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
useJUnitPlatform()
}
androidComponents {
onVariants(selector().withBuildType("release")) { variant ->
val variantName = variant.name
val variantNameCapitalized = variantName.replaceFirstChar(Char::titlecase)
val stripTaskName = "strip${variantNameCapitalized}DnsjavaServiceDescriptor"
val mergeTaskName = "merge${variantNameCapitalized}JavaResource"
val minifyTaskName = "minify${variantNameCapitalized}WithR8"
val mergedJar =
layout.buildDirectory.file(
"intermediates/merged_java_res/$variantName/$mergeTaskName/base.jar",
)
onVariants(selector().withBuildType("release")) { variant ->
val variantName = variant.name
val variantNameCapitalized = variantName.replaceFirstChar(Char::titlecase)
val stripTaskName = "strip${variantNameCapitalized}DnsjavaServiceDescriptor"
val mergeTaskName = "merge${variantNameCapitalized}JavaResource"
val minifyTaskName = "minify${variantNameCapitalized}WithR8"
val mergedJar =
layout.buildDirectory.file(
"intermediates/merged_java_res/$variantName/$mergeTaskName/base.jar",
)
val stripTask =
tasks.register(stripTaskName) {
inputs.file(mergedJar)
outputs.file(mergedJar)
val stripTask =
tasks.register(stripTaskName) {
inputs.file(mergedJar)
outputs.file(mergedJar)
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
}
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
}
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
}
}
tasks.matching { it.name == mergeTaskName }.configureEach {
finalizedBy(stripTask)
}
tasks.matching { it.name == minifyTaskName }.configureEach {
dependsOn(stripTask)
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
}
}
tasks.matching { it.name == mergeTaskName }.configureEach {
finalizedBy(stripTask)
}
tasks.matching { it.name == minifyTaskName }.configureEach {
dependsOn(stripTask)
}
}
}

View File

@@ -12,8 +12,6 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission
@@ -21,7 +19,6 @@
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
@@ -41,7 +38,7 @@
<application
android:name=".NodeApp"
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View File

@@ -8,9 +8,8 @@ object DeviceNames {
fun bestDefaultNodeName(context: Context): String {
val deviceName =
runCatching {
Settings.Global.getString(context.contentResolver, "device_name")
}
.getOrNull()
Settings.Global.getString(context.contentResolver, "device_name")
}.getOrNull()
?.trim()
.orEmpty()

View File

@@ -1,6 +1,8 @@
package ai.openclaw.app
enum class LocationMode(val rawValue: String) {
enum class LocationMode(
val rawValue: String,
) {
Off("off"),
WhileUsing("whileUsing"),
;

View File

@@ -1,18 +1,18 @@
package ai.openclaw.app
import ai.openclaw.app.ui.OpenClawTheme
import ai.openclaw.app.ui.RootScreen
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.core.view.WindowCompat
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import ai.openclaw.app.ui.RootScreen
import ai.openclaw.app.ui.OpenClawTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {

View File

@@ -1,9 +1,5 @@
package ai.openclaw.app
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
@@ -13,6 +9,10 @@ import ai.openclaw.app.node.CameraCaptureManager
import ai.openclaw.app.node.CanvasController
import ai.openclaw.app.node.SmsManager
import ai.openclaw.app.voice.VoiceConversationEntry
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -22,7 +22,9 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(app: Application) : AndroidViewModel(app) {
class MainViewModel(
app: Application,
) : AndroidViewModel(app) {
private val nodeApp = app as NodeApp
private val prefs = nodeApp.prefs
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
@@ -143,7 +145,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
val sms: SmsManager
get() = ensureRuntime().sms
fun attachRuntimeUi(owner: LifecycleOwner, permissionRequester: PermissionRequester) {
fun attachRuntimeUi(
owner: LifecycleOwner,
permissionRequester: PermissionRequester,
) {
val runtime = runtimeRef.value ?: return
runtime.camera.attachLifecycleOwner(owner)
runtime.camera.attachPermissionRequester(permissionRequester)
@@ -245,9 +250,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
enabled: Boolean,
start: String,
end: String,
): Boolean {
return ensureRuntime().setNotificationForwardingQuietHours(enabled = enabled, start = start, end = end)
}
): Boolean = ensureRuntime().setNotificationForwardingQuietHours(enabled = enabled, start = start, end = end)
fun setNotificationForwardingMaxEventsPerMinute(value: Int) {
ensureRuntime().setNotificationForwardingMaxEventsPerMinute(value)
@@ -340,9 +343,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
ensureRuntime().handleCanvasA2UIActionFromWebView(payloadJson)
}
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
return ensureRuntime().isTrustedCanvasActionUrl(rawUrl)
}
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean = ensureRuntime().isTrustedCanvasActionUrl(rawUrl)
fun requestCanvasRehydrate(source: String = "screen_tab") {
ensureRuntime().requestCanvasRehydrate(source = source, force = true)
@@ -376,7 +377,11 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
ensureRuntime().abortChat()
}
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
fun sendChat(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
) {
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
}
@@ -384,11 +389,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return ensureRuntime().sendChatAwaitAcceptance(
): Boolean =
ensureRuntime().sendChatAwaitAcceptance(
message = message,
thinking = thinking,
attachments = attachments,
)
}
}

View File

@@ -21,13 +21,15 @@ class NodeApp : Application() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
StrictMode.ThreadPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
StrictMode.VmPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),

View File

@@ -140,10 +140,14 @@ class NodeForegroundService : Service() {
mgr.createNotificationChannel(channel)
}
private fun buildNotification(title: String, text: String): Notification {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
private fun buildNotification(
title: String,
text: String,
): Notification {
val launchIntent =
Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val launchPending =
PendingIntent.getActivity(
this,
@@ -161,7 +165,8 @@ class NodeForegroundService : Service() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
return NotificationCompat
.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(text)
@@ -233,8 +238,8 @@ internal fun voiceNotificationSuffix(
manualMicListening: Boolean,
talkListening: Boolean,
talkSpeaking: Boolean,
): String {
return when (mode) {
): String =
when (mode) {
VoiceCaptureMode.TalkMode ->
when {
talkSpeaking -> " · Talk: Speaking"
@@ -249,11 +254,11 @@ internal fun voiceNotificationSuffix(
}
VoiceCaptureMode.Off -> ""
}
}
private fun String?.toVoiceCaptureMode(): VoiceCaptureMode {
return VoiceCaptureMode.entries.firstOrNull { it.name == this } ?: VoiceCaptureMode.Off
}
private fun String?.toVoiceCaptureMode(): VoiceCaptureMode =
VoiceCaptureMode.entries.firstOrNull {
it.name == this
} ?: VoiceCaptureMode.Off
private data class VoiceNotificationBase(
val status: String,

View File

@@ -1,11 +1,5 @@
package ai.openclaw.app
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.SystemClock
import android.util.Log
import androidx.core.content.ContextCompat
import ai.openclaw.app.chat.ChatController
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
@@ -24,6 +18,12 @@ import ai.openclaw.app.protocol.OpenClawCanvasA2UIAction
import ai.openclaw.app.voice.MicCaptureManager
import ai.openclaw.app.voice.TalkModeManager
import ai.openclaw.app.voice.VoiceConversationEntry
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.SystemClock
import android.util.Log
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -75,122 +75,137 @@ class NodeRuntime(
private var connectedEndpoint: GatewayEndpoint? = null
private var activeGatewayAuth: GatewayConnectAuth? = null
private val cameraHandler: CameraHandler = CameraHandler(
appContext = appContext,
camera = camera,
externalAudioCaptureActive = externalAudioCaptureActive,
showCameraHud = ::showCameraHud,
triggerCameraFlash = ::triggerCameraFlash,
invokeErrorFromThrowable = { invokeErrorFromThrowable(it) },
)
private val cameraHandler: CameraHandler =
CameraHandler(
appContext = appContext,
camera = camera,
externalAudioCaptureActive = externalAudioCaptureActive,
showCameraHud = ::showCameraHud,
triggerCameraFlash = ::triggerCameraFlash,
invokeErrorFromThrowable = { invokeErrorFromThrowable(it) },
)
private val debugHandler: DebugHandler = DebugHandler(
appContext = appContext,
identityStore = identityStore,
)
private val debugHandler: DebugHandler =
DebugHandler(
appContext = appContext,
identityStore = identityStore,
)
private val locationHandler: LocationHandler = LocationHandler(
appContext = appContext,
location = location,
json = json,
isForeground = { _isForeground.value },
locationPreciseEnabled = { locationPreciseEnabled.value },
)
private val locationHandler: LocationHandler =
LocationHandler(
appContext = appContext,
location = location,
json = json,
isForeground = { _isForeground.value },
locationPreciseEnabled = { locationPreciseEnabled.value },
)
private val deviceHandler: DeviceHandler = DeviceHandler(
appContext = appContext,
smsEnabled = BuildConfig.OPENCLAW_ENABLE_SMS,
callLogEnabled = BuildConfig.OPENCLAW_ENABLE_CALL_LOG,
)
private val deviceHandler: DeviceHandler =
DeviceHandler(
appContext = appContext,
smsEnabled = BuildConfig.OPENCLAW_ENABLE_SMS,
callLogEnabled = BuildConfig.OPENCLAW_ENABLE_CALL_LOG,
)
private val notificationsHandler: NotificationsHandler = NotificationsHandler(
appContext = appContext,
)
private val notificationsHandler: NotificationsHandler =
NotificationsHandler(
appContext = appContext,
)
private val systemHandler: SystemHandler = SystemHandler(
appContext = appContext,
)
private val systemHandler: SystemHandler =
SystemHandler(
appContext = appContext,
)
private val photosHandler: PhotosHandler = PhotosHandler(
appContext = appContext,
)
private val photosHandler: PhotosHandler =
PhotosHandler(
appContext = appContext,
)
private val contactsHandler: ContactsHandler = ContactsHandler(
appContext = appContext,
)
private val contactsHandler: ContactsHandler =
ContactsHandler(
appContext = appContext,
)
private val calendarHandler: CalendarHandler = CalendarHandler(
appContext = appContext,
)
private val calendarHandler: CalendarHandler =
CalendarHandler(
appContext = appContext,
)
private val callLogHandler: CallLogHandler = CallLogHandler(
appContext = appContext,
)
private val callLogHandler: CallLogHandler =
CallLogHandler(
appContext = appContext,
)
private val motionHandler: MotionHandler = MotionHandler(
appContext = appContext,
)
private val motionHandler: MotionHandler =
MotionHandler(
appContext = appContext,
)
private val smsHandlerImpl: SmsHandler = SmsHandler(
sms = sms,
)
private val smsHandlerImpl: SmsHandler =
SmsHandler(
sms = sms,
)
private val a2uiHandler: A2UIHandler = A2UIHandler(
canvas = canvas,
json = json,
getNodeCanvasHostUrl = { nodeSession.currentCanvasHostUrl() },
getOperatorCanvasHostUrl = { operatorSession.currentCanvasHostUrl() },
)
private val a2uiHandler: A2UIHandler =
A2UIHandler(
canvas = canvas,
json = json,
getNodeCanvasHostUrl = { nodeSession.currentCanvasHostUrl() },
getOperatorCanvasHostUrl = { operatorSession.currentCanvasHostUrl() },
)
private val connectionManager: ConnectionManager = ConnectionManager(
prefs = prefs,
cameraEnabled = { cameraEnabled.value },
locationMode = { locationMode.value },
voiceWakeMode = { VoiceWakeMode.Off },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
sendSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canSendSms() },
readSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canReadSms() },
smsSearchPossible = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.hasTelephonyFeature() },
callLogAvailable = { BuildConfig.OPENCLAW_ENABLE_CALL_LOG },
hasRecordAudioPermission = { hasRecordAudioPermission() },
manualTls = { manualTls.value },
)
private val connectionManager: ConnectionManager =
ConnectionManager(
prefs = prefs,
cameraEnabled = { cameraEnabled.value },
locationMode = { locationMode.value },
voiceWakeMode = { VoiceWakeMode.Off },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
sendSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canSendSms() },
readSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canReadSms() },
smsSearchPossible = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.hasTelephonyFeature() },
callLogAvailable = { BuildConfig.OPENCLAW_ENABLE_CALL_LOG },
hasRecordAudioPermission = { hasRecordAudioPermission() },
manualTls = { manualTls.value },
)
private val invokeDispatcher: InvokeDispatcher = InvokeDispatcher(
canvas = canvas,
cameraHandler = cameraHandler,
locationHandler = locationHandler,
deviceHandler = deviceHandler,
notificationsHandler = notificationsHandler,
systemHandler = systemHandler,
photosHandler = photosHandler,
contactsHandler = contactsHandler,
calendarHandler = calendarHandler,
motionHandler = motionHandler,
smsHandler = smsHandlerImpl,
a2uiHandler = a2uiHandler,
debugHandler = debugHandler,
callLogHandler = callLogHandler,
isForeground = { _isForeground.value },
cameraEnabled = { cameraEnabled.value },
locationEnabled = { locationMode.value != LocationMode.Off },
sendSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canSendSms() },
readSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canReadSms() },
smsFeatureEnabled = { BuildConfig.OPENCLAW_ENABLE_SMS },
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
callLogAvailable = { BuildConfig.OPENCLAW_ENABLE_CALL_LOG },
debugBuild = { BuildConfig.DEBUG },
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
onCanvasA2uiPush = {
_canvasA2uiHydrated.value = true
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
},
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
private val invokeDispatcher: InvokeDispatcher =
InvokeDispatcher(
canvas = canvas,
cameraHandler = cameraHandler,
locationHandler = locationHandler,
deviceHandler = deviceHandler,
notificationsHandler = notificationsHandler,
systemHandler = systemHandler,
photosHandler = photosHandler,
contactsHandler = contactsHandler,
calendarHandler = calendarHandler,
motionHandler = motionHandler,
smsHandler = smsHandlerImpl,
a2uiHandler = a2uiHandler,
debugHandler = debugHandler,
callLogHandler = callLogHandler,
isForeground = { _isForeground.value },
cameraEnabled = { cameraEnabled.value },
locationEnabled = { locationMode.value != LocationMode.Off },
sendSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canSendSms() },
readSmsAvailable = { BuildConfig.OPENCLAW_ENABLE_SMS && sms.canReadSms() },
smsFeatureEnabled = { BuildConfig.OPENCLAW_ENABLE_SMS },
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
callLogAvailable = { BuildConfig.OPENCLAW_ENABLE_CALL_LOG },
debugBuild = { BuildConfig.DEBUG },
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
onCanvasA2uiPush = {
_canvasA2uiHydrated.value = true
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
},
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
data class GatewayTrustPrompt(
val endpoint: GatewayEndpoint,
@@ -247,6 +262,8 @@ class NodeRuntime(
private var gatewayAgents: List<GatewayAgentSummary> = emptyList()
private var didAutoRequestCanvasRehydrate = false
private val canvasRehydrateSeq = AtomicLong(0)
@Volatile private var nodePresenceAliveLastSuccessAtMs: Long? = null
private var operatorConnected = false
private var operatorStatusText: String = "Offline"
private var nodeStatusText: String = "Offline"
@@ -302,6 +319,7 @@ class NodeRuntime(
_canvasRehydrateErrorText.value = null
updateStatus()
showLocalCanvasOnConnect()
publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Connect)
val endpoint = connectedEndpoint
val auth = activeGatewayAuth
if (endpoint != null && auth != null) {
@@ -344,21 +362,22 @@ class NodeRuntime(
).also {
it.applyMainSessionKey(_mainSessionKey.value)
}
private val voiceReplySpeakerLazy: Lazy<TalkModeManager> = lazy {
// Reuse the existing TalkMode speech engine for native Android TTS playback
// without enabling the legacy talk capture loop.
TalkModeManager(
context = appContext,
scope = scope,
session = operatorSession,
supportsChatSubscribe = false,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
).also { speaker ->
speaker.setPlaybackEnabled(prefs.speakerEnabled.value)
private val voiceReplySpeakerLazy: Lazy<TalkModeManager> =
lazy {
// Reuse the existing TalkMode speech engine for native Android TTS playback
// without enabling the legacy talk capture loop.
TalkModeManager(
context = appContext,
scope = scope,
session = operatorSession,
supportsChatSubscribe = false,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
).also { speaker ->
speaker.setPlaybackEnabled(prefs.speakerEnabled.value)
}
}
}
private val voiceReplySpeaker: TalkModeManager
get() = voiceReplySpeakerLazy.value
@@ -504,7 +523,10 @@ class NodeRuntime(
}
}
fun requestCanvasRehydrate(source: String = "manual", force: Boolean = true) {
fun requestCanvasRehydrate(
source: String = "manual",
force: Boolean = true,
) {
scope.launch {
if (!_nodeConnected.value) {
_canvasRehydratePending.value = false
@@ -567,16 +589,22 @@ class NodeRuntime(
val manualTls: StateFlow<Boolean> = prefs.manualTls
val gatewayToken: StateFlow<String> = prefs.gatewayToken
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
fun setGatewayToken(value: String) = prefs.setGatewayToken(value)
fun setGatewayBootstrapToken(value: String) = prefs.setGatewayBootstrapToken(value)
fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value)
fun resetGatewaySetupAuth() {
prefs.clearGatewaySetupAuth()
val deviceId = identityStore.loadOrCreate().deviceId
deviceAuthStore.clearToken(deviceId, "node")
deviceAuthStore.clearToken(deviceId, "operator")
}
fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val notificationForwardingEnabled: StateFlow<Boolean> = prefs.notificationForwardingEnabled
@@ -649,6 +677,60 @@ class NodeRuntime(
reconnectPreferredGatewayOnForeground()
} else {
stopManualVoiceSession()
publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Background, throttleRecentSuccess = true)
}
}
private fun publishNodePresenceAliveBeacon(
trigger: NodePresenceAliveBeacon.Trigger,
throttleRecentSuccess: Boolean = false,
) {
scope.launch {
sendNodePresenceAliveBeacon(trigger = trigger, throttleRecentSuccess = throttleRecentSuccess)
}
}
private suspend fun sendNodePresenceAliveBeacon(
trigger: NodePresenceAliveBeacon.Trigger,
throttleRecentSuccess: Boolean,
) {
if (!_nodeConnected.value) return
val nowMs = System.currentTimeMillis()
if (
throttleRecentSuccess &&
NodePresenceAliveBeacon.shouldSkipRecentSuccess(
nowMs = nowMs,
lastSuccessAtMs = nodePresenceAliveLastSuccessAtMs,
)
) {
return
}
val client = connectionManager.buildClientInfo(clientId = "openclaw-android", clientMode = "node")
val payloadJson =
NodePresenceAliveBeacon.makePayloadJson(
trigger = trigger,
sentAtMs = nowMs,
displayName = client.displayName?.trim()?.takeIf { it.isNotEmpty() } ?: "Android",
version = client.version,
platform = NodePresenceAliveBeacon.androidPlatformLabel(),
deviceFamily = client.deviceFamily,
modelIdentifier = client.modelIdentifier,
)
val result =
nodeSession.sendNodeEventDetailed(
event = NodePresenceAliveBeacon.EVENT_NAME,
payloadJson = payloadJson,
)
if (!result.ok) return
val response = NodePresenceAliveBeacon.decodeResponse(result.payloadJson)
if (response?.handled == true) {
nodePresenceAliveLastSuccessAtMs = nowMs
} else {
Log.d(
"OpenClawNode",
"node.presence.alive not handled: ${NodePresenceAliveBeacon.sanitizeReasonForLog(response?.reason)}",
)
}
}
@@ -748,9 +830,7 @@ class NodeRuntime(
enabled: Boolean,
start: String,
end: String,
): Boolean {
return prefs.setNotificationForwardingQuietHours(enabled = enabled, start = start, end = end)
}
): Boolean = prefs.setNotificationForwardingQuietHours(enabled = enabled, start = start, end = end)
fun setNotificationForwardingMaxEventsPerMinute(value: Int) {
prefs.setNotificationForwardingMaxEventsPerMinute(value)
@@ -927,10 +1007,11 @@ class NodeRuntime(
_statusText.value = "Verify gateway TLS fingerprint…"
scope.launch {
val tlsProbe = tlsFingerprintProbe(endpoint.host, endpoint.port)
val fp = tlsProbe.fingerprintSha256 ?: run {
_statusText.value = gatewayTlsProbeFailureMessage(tlsProbe.failure)
return@launch
}
val fp =
tlsProbe.fingerprintSha256 ?: run {
_statusText.value = gatewayTlsProbeFailureMessage(tlsProbe.failure)
return@launch
}
_pendingGatewayTrust.value =
GatewayTrustPrompt(endpoint = endpoint, fingerprintSha256 = fp, auth = auth)
}
@@ -955,14 +1036,13 @@ class NodeRuntime(
beginConnect(endpoint = endpoint, auth = resolveGatewayConnectAuth(auth))
}
internal fun resolveGatewayConnectAuth(explicitAuth: GatewayConnectAuth? = null): GatewayConnectAuth {
return explicitAuth
internal fun resolveGatewayConnectAuth(explicitAuth: GatewayConnectAuth? = null): GatewayConnectAuth =
explicitAuth
?: GatewayConnectAuth(
token = prefs.loadGatewayToken(),
bootstrapToken = prefs.loadGatewayBootstrapToken(),
password = prefs.loadGatewayPassword(),
)
}
fun acceptGatewayTrustPrompt() {
val prompt = _pendingGatewayTrust.value ?: return
@@ -976,21 +1056,19 @@ class NodeRuntime(
_statusText.value = "Offline"
}
private fun gatewayTlsProbeFailureMessage(failure: GatewayTlsProbeFailure?): String {
return when (failure) {
private fun gatewayTlsProbeFailureMessage(failure: GatewayTlsProbeFailure?): String =
when (failure) {
GatewayTlsProbeFailure.TLS_UNAVAILABLE ->
"Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected."
GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE, null ->
"Failed: couldn't reach the secure gateway endpoint for this host."
}
}
private fun hasRecordAudioPermission(): Boolean {
return (
private fun hasRecordAudioPermission(): Boolean =
(
ContextCompat.checkSelfPermission(appContext, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
)
}
)
fun connectManual() {
val host = manualHost.value.trim()
@@ -1053,15 +1131,26 @@ class NodeRuntime(
}
val userActionObj = (root["userAction"] as? JsonObject) ?: root
val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
java.util.UUID.randomUUID().toString()
}
val actionId =
(userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
java.util.UUID
.randomUUID()
.toString()
}
val name = OpenClawCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val surfaceId =
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
(userActionObj["surfaceId"] as? JsonPrimitive)
?.content
?.trim()
.orEmpty()
.ifEmpty { "main" }
val sourceComponentId =
(userActionObj["sourceComponentId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "-" }
(userActionObj["sourceComponentId"] as? JsonPrimitive)
?.content
?.trim()
.orEmpty()
.ifEmpty { "-" }
val contextJson = (userActionObj["context"] as? JsonObject)?.toString()
val sessionKey = resolveMainSessionKey()
@@ -1112,9 +1201,7 @@ class NodeRuntime(
}
}
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
return a2uiHandler.isTrustedCanvasActionUrl(rawUrl)
}
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean = a2uiHandler.isTrustedCanvasActionUrl(rawUrl)
fun loadChat(sessionKey: String) {
val key = sessionKey.trim().ifEmpty { resolveMainSessionKey() }
@@ -1141,7 +1228,11 @@ class NodeRuntime(
chat.abort()
}
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
fun sendChat(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
) {
chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments)
}
@@ -1149,11 +1240,12 @@ class NodeRuntime(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
}
): Boolean = chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
private fun handleGatewayEvent(event: String, payloadJson: String?) {
private fun handleGatewayEvent(
event: String,
payloadJson: String?,
) {
micCapture.handleGatewayEvent(event, payloadJson)
talkMode.handleGatewayEvent(event, payloadJson)
chat.handleGatewayEvent(event, payloadJson)
@@ -1199,7 +1291,12 @@ class NodeRuntime(
val id = obj["id"].asStringOrNull()?.trim().orEmpty()
if (id.isEmpty()) return@mapNotNull null
val name = obj["name"].asStringOrNull()?.trim()
val emoji = obj["identity"].asObjectOrNull()?.get("emoji").asStringOrNull()?.trim()
val emoji =
obj["identity"]
.asObjectOrNull()
?.get("emoji")
.asStringOrNull()
?.trim()
GatewayAgentSummary(
id = id,
name = name?.takeIf { it.isNotEmpty() },
@@ -1356,7 +1453,11 @@ class NodeRuntime(
_cameraFlashToken.value = SystemClock.elapsedRealtimeNanos()
}
private fun showCameraHud(message: String, kind: CameraHudKind, autoHideMs: Long? = null) {
private fun showCameraHud(
message: String,
kind: CameraHudKind,
autoHideMs: Long? = null,
) {
val token = cameraHudSeq.incrementAndGet()
_cameraHud.value = CameraHudState(token = token, kind = kind, message = message)
@@ -1367,7 +1468,6 @@ class NodeRuntime(
}
}
}
}
internal fun resolveOperatorSessionConnectAuth(
@@ -1416,9 +1516,7 @@ internal fun resolveOperatorSessionConnectAuth(
internal fun shouldConnectOperatorSession(
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): Boolean {
return resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
}
): Boolean = resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
private enum class HomeCanvasGatewayState {
Connected,

View File

@@ -3,15 +3,15 @@ package ai.openclaw.app
import java.time.Instant
import java.time.ZoneId
enum class NotificationPackageFilterMode(val rawValue: String) {
enum class NotificationPackageFilterMode(
val rawValue: String,
) {
Allowlist("allowlist"),
Blocklist("blocklist"),
;
companion object {
fun fromRawValue(raw: String?): NotificationPackageFilterMode {
return entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Blocklist
}
fun fromRawValue(raw: String?): NotificationPackageFilterMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Blocklist
}
}
@@ -50,7 +50,8 @@ internal fun NotificationForwardingPolicy.isWithinQuietHours(
return true
}
val now =
Instant.ofEpochMilli(nowEpochMs)
Instant
.ofEpochMilli(nowEpochMs)
.atZone(zoneId)
.toLocalTime()
val nowMinutes = now.hour * 60 + now.minute
@@ -82,7 +83,10 @@ internal class NotificationBurstLimiter {
private var windowStartMs: Long = -1L
private var eventsInWindow: Int = 0
fun allow(nowEpochMs: Long, maxEventsPerMinute: Int): Boolean {
fun allow(
nowEpochMs: Long,
maxEventsPerMinute: Int,
): Boolean {
if (maxEventsPerMinute <= 0) {
return false
}

View File

@@ -1,30 +1,32 @@
package ai.openclaw.app
import android.content.pm.PackageManager
import android.content.Intent
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
class PermissionRequester(private val activity: ComponentActivity) {
class PermissionRequester(
private val activity: ComponentActivity,
) {
private val mutex = Mutex()
private var pending: CompletableDeferred<Map<String, Boolean>>? = null
private val mainHandler = Handler(Looper.getMainLooper())
@@ -74,10 +76,10 @@ class PermissionRequester(private val activity: ComponentActivity) {
// Merge: if something was already granted, treat it as granted even if launcher omitted it.
val merged =
permissions.associateWith { perm ->
val nowGranted =
ContextCompat.checkSelfPermission(activity, perm) == PackageManager.PERMISSION_GRANTED
result[perm] == true || nowGranted
}
val nowGranted =
ContextCompat.checkSelfPermission(activity, perm) == PackageManager.PERMISSION_GRANTED
result[perm] == true || nowGranted
}
val denied =
merged.filterValues { !it }.keys.filter {
@@ -104,6 +106,7 @@ class PermissionRequester(private val activity: ComponentActivity) {
observer?.let(lifecycle::removeObserver)
observer = null
}
fun finish(result: Boolean?) {
if (!finished.compareAndSet(false, true)) return
removeObserver()
@@ -125,7 +128,8 @@ class PermissionRequester(private val activity: ComponentActivity) {
}
}
dialog =
AlertDialog.Builder(activity)
AlertDialog
.Builder(activity)
.setTitle("Permission required")
.setMessage(buildRationaleMessage(permissions))
.setPositiveButton("Continue") { _, _ -> finish(true) }
@@ -154,7 +158,8 @@ class PermissionRequester(private val activity: ComponentActivity) {
observer = actualObserver
lifecycle.addObserver(actualObserver)
dialog =
AlertDialog.Builder(activity)
AlertDialog
.Builder(activity)
.setTitle("Enable permission in Settings")
.setMessage(buildSettingsMessage(permissions))
.setPositiveButton("Open Settings") { _, _ ->
@@ -165,8 +170,7 @@ class PermissionRequester(private val activity: ComponentActivity) {
Uri.fromParts("package", activity.packageName, null),
)
activity.startActivity(intent)
}
.setNegativeButton("Cancel", null)
}.setNegativeButton("Cancel", null)
.setOnDismissListener { removeObserver() }
.show()
}

View File

@@ -46,7 +46,8 @@ class SecurePrefs(
appContext.getSharedPreferences(plainPrefsName, Context.MODE_PRIVATE)
private val masterKey by lazy {
MasterKey.Builder(appContext)
MasterKey
.Builder(appContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
}
@@ -420,16 +421,20 @@ class SecurePrefs(
return plainPrefs.getString(key, null)?.trim()?.takeIf { it.isNotEmpty() }
}
fun saveGatewayTlsFingerprint(stableId: String, fingerprint: String) {
fun saveGatewayTlsFingerprint(
stableId: String,
fingerprint: String,
) {
val key = "gateway.tls.$stableId"
plainPrefs.edit { putString(key, fingerprint.trim()) }
}
fun getString(key: String): String? {
return securePrefs.getString(key, null)
}
fun getString(key: String): String? = securePrefs.getString(key, null)
fun putString(key: String, value: String) {
fun putString(
key: String,
value: String,
) {
securePrefs.edit { putString(key, value) }
}
@@ -437,15 +442,17 @@ class SecurePrefs(
securePrefs.edit { remove(key) }
}
private fun createSecurePrefs(context: Context, name: String): SharedPreferences {
return EncryptedSharedPreferences.create(
private fun createSecurePrefs(
context: Context,
name: String,
): SharedPreferences =
EncryptedSharedPreferences.create(
context,
name,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
}
private fun loadOrCreateInstanceId(): String {
val existing = plainPrefs.getString("node.instanceId", null)?.trim()
@@ -504,8 +511,7 @@ class SecurePrefs(
is JsonPrimitive -> item.content.trim().takeIf { it.isNotEmpty() }
else -> null
}
}
.toSet()
}.toSet()
} catch (_: Throwable) {
emptySet()
}

View File

@@ -15,10 +15,17 @@ internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
val trimmed = raw?.trim().orEmpty()
if (!trimmed.startsWith("agent:")) return null
return trimmed.removePrefix("agent:").substringBefore(':').trim().ifEmpty { null }
return trimmed
.removePrefix("agent:")
.substringBefore(':')
.trim()
.ifEmpty { null }
}
internal fun buildNodeMainSessionKey(deviceId: String, agentId: String?): String {
internal fun buildNodeMainSessionKey(
deviceId: String,
agentId: String?,
): String {
val resolvedAgentId = agentId?.trim().orEmpty().ifEmpty { "main" }
return "agent:$resolvedAgentId:node-${deviceId.take(12)}"
}

View File

@@ -1,14 +1,14 @@
package ai.openclaw.app
enum class VoiceWakeMode(val rawValue: String) {
enum class VoiceWakeMode(
val rawValue: String,
) {
Off("off"),
Foreground("foreground"),
Always("always"),
;
companion object {
fun fromRawValue(raw: String?): VoiceWakeMode {
return entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Foreground
}
fun fromRawValue(raw: String?): VoiceWakeMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Foreground
}
}

View File

@@ -4,18 +4,26 @@ object WakeWords {
const val maxWords: Int = 32
const val maxWordLength: Int = 64
fun parseCommaSeparated(input: String): List<String> {
return input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
}
fun parseCommaSeparated(input: String): List<String> = input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
fun parseIfChanged(input: String, current: List<String>): List<String>? {
fun parseIfChanged(
input: String,
current: List<String>,
): List<String>? {
val parsed = parseCommaSeparated(input)
return if (parsed == current) null else parsed
}
fun sanitize(words: List<String>, defaults: List<String>): List<String> {
fun sanitize(
words: List<String>,
defaults: List<String>,
): List<String> {
val cleaned =
words.map { it.trim() }.filter { it.isNotEmpty() }.take(maxWords).map { it.take(maxWordLength) }
words
.map { it.trim() }
.filter { it.isNotEmpty() }
.take(maxWords)
.map { it.take(maxWordLength) }
return cleaned.ifEmpty { defaults }
}
}

View File

@@ -1,8 +1,6 @@
package ai.openclaw.app.chat
import ai.openclaw.app.gateway.GatewaySession
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -17,6 +15,8 @@ import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class ChatController(
private val scope: CoroutineScope,
@@ -173,12 +173,12 @@ class ChatController(
}
_messages.value =
_messages.value +
ChatMessage(
id = UUID.randomUUID().toString(),
role = "user",
content = userContent,
timestampMs = System.currentTimeMillis(),
)
ChatMessage(
id = UUID.randomUUID().toString(),
role = "user",
content = userContent,
timestampMs = System.currentTimeMillis(),
)
armPendingRunTimeout(runId)
synchronized(pendingRuns) {
@@ -255,7 +255,10 @@ class ChatController(
}
}
fun handleGatewayEvent(event: String, payloadJson: String?) {
fun handleGatewayEvent(
event: String,
payloadJson: String?,
) {
when (event) {
"tick" -> {
scope.launch { pollHealthIfNeeded(force = false) }
@@ -279,7 +282,10 @@ class ChatController(
}
}
private suspend fun bootstrap(forceHealth: Boolean, refreshSessions: Boolean) {
private suspend fun bootstrap(
forceHealth: Boolean,
refreshSessions: Boolean,
) {
_errorText.value = null
_healthOk.value = false
clearPendingRuns()
@@ -298,7 +304,10 @@ class ChatController(
val history = parseHistory(historyJson, sessionKey = key, previousMessages = _messages.value)
_messages.value = history.messages
_sessionId.value = history.sessionId
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
history.thinkingLevel
?.trim()
?.takeIf { it.isNotEmpty() }
?.let { _thinkingLevel.value = it }
pollHealthIfNeeded(force = forceHealth)
if (refreshSessions) {
@@ -371,7 +380,10 @@ class ChatController(
val history = parseHistory(historyJson, sessionKey = _sessionKey.value, previousMessages = _messages.value)
_messages.value = history.messages
_sessionId.value = history.sessionId
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
history.thinkingLevel
?.trim()
?.takeIf { it.isNotEmpty() }
?.let { _thinkingLevel.value = it }
} catch (_: Throwable) {
// best-effort
}
@@ -542,22 +554,24 @@ class ChatController(
}
}
private fun parseRunId(resJson: String): String? {
return try {
json.parseToJsonElement(resJson).asObjectOrNull()?.get("runId").asStringOrNull()
private fun parseRunId(resJson: String): String? =
try {
json
.parseToJsonElement(resJson)
.asObjectOrNull()
?.get("runId")
.asStringOrNull()
} catch (_: Throwable) {
null
}
}
private fun normalizeThinking(raw: String): String {
return when (raw.trim().lowercase()) {
private fun normalizeThinking(raw: String): String =
when (raw.trim().lowercase()) {
"low" -> "low"
"medium" -> "medium"
"high" -> "high"
else -> "off"
}
}
}
internal data class MainSessionState(
@@ -582,7 +596,10 @@ internal fun applyMainSessionKey(
)
}
internal fun reconcileMessageIds(previous: List<ChatMessage>, incoming: List<ChatMessage>): List<ChatMessage> {
internal fun reconcileMessageIds(
previous: List<ChatMessage>,
incoming: List<ChatMessage>,
): List<ChatMessage> {
if (previous.isEmpty() || incoming.isEmpty()) return incoming
val idsByKey = LinkedHashMap<String, ArrayDeque<String>>()
@@ -613,9 +630,15 @@ internal fun messageIdentityKey(message: ChatMessage): String? {
listOf(
part.type.trim().lowercase(),
part.text?.trim().orEmpty(),
part.mimeType?.trim()?.lowercase().orEmpty(),
part.mimeType
?.trim()
?.lowercase()
.orEmpty(),
part.fileName?.trim().orEmpty(),
part.base64?.hashCode()?.toString().orEmpty(),
part.base64
?.hashCode()
?.toString()
.orEmpty(),
).joinToString(separator = "\u001F")
}

View File

@@ -19,21 +19,44 @@ private data class PersistedDeviceAuthMetadata(
)
interface DeviceAuthTokenStore {
fun loadEntry(deviceId: String, role: String): DeviceAuthEntry?
fun loadToken(deviceId: String, role: String): String? = loadEntry(deviceId, role)?.token
fun saveToken(deviceId: String, role: String, token: String, scopes: List<String> = emptyList())
fun clearToken(deviceId: String, role: String)
fun loadEntry(
deviceId: String,
role: String,
): DeviceAuthEntry?
fun loadToken(
deviceId: String,
role: String,
): String? = loadEntry(deviceId, role)?.token
fun saveToken(
deviceId: String,
role: String,
token: String,
scopes: List<String> = emptyList(),
)
fun clearToken(
deviceId: String,
role: String,
)
}
class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
class DeviceAuthStore(
private val prefs: SecurePrefs,
) : DeviceAuthTokenStore {
private val json = Json { ignoreUnknownKeys = true }
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? {
override fun loadEntry(
deviceId: String,
role: String,
): DeviceAuthEntry? {
val key = tokenKey(deviceId, role)
val token = prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
val normalizedRole = normalizeRole(role)
val metadata =
prefs.getString(metadataKey(deviceId, role))
prefs
.getString(metadataKey(deviceId, role))
?.let { raw ->
runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull()
}
@@ -45,7 +68,12 @@ class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
)
}
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
override fun saveToken(
deviceId: String,
role: String,
token: String,
scopes: List<String>,
) {
val normalizedScopes = normalizeScopes(scopes)
val key = tokenKey(deviceId, role)
prefs.putString(key, token.trim())
@@ -60,19 +88,28 @@ class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
)
}
override fun clearToken(deviceId: String, role: String) {
override fun clearToken(
deviceId: String,
role: String,
) {
val key = tokenKey(deviceId, role)
prefs.remove(key)
prefs.remove(metadataKey(deviceId, role))
}
private fun tokenKey(deviceId: String, role: String): String {
private fun tokenKey(
deviceId: String,
role: String,
): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
}
private fun metadataKey(deviceId: String, role: String): String {
private fun metadataKey(
deviceId: String,
role: String,
): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceTokenMeta.$normalizedDevice.$normalizedRole"
@@ -82,11 +119,10 @@ class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
private fun normalizeRole(role: String): String = role.trim().lowercase()
private fun normalizeScopes(scopes: List<String>): List<String> {
return scopes
private fun normalizeScopes(scopes: List<String>): List<String> =
scopes
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
}
}

View File

@@ -2,10 +2,10 @@ package ai.openclaw.app.gateway
import android.content.Context
import android.util.Base64
import java.io.File
import java.security.MessageDigest
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.File
import java.security.MessageDigest
@Serializable
data class DeviceIdentity(
@@ -15,9 +15,12 @@ data class DeviceIdentity(
val createdAtMs: Long,
)
class DeviceIdentityStore(context: Context) {
class DeviceIdentityStore(
context: Context,
) {
private val json = Json { ignoreUnknownKeys = true }
private val identityFile = File(context.filesDir, "openclaw/identity/device.json")
@Volatile private var cachedIdentity: DeviceIdentity? = null
@Synchronized
@@ -41,15 +44,27 @@ class DeviceIdentityStore(context: Context) {
return fresh
}
fun signPayload(payload: String, identity: DeviceIdentity): String? {
return try {
fun signPayload(
payload: String,
identity: DeviceIdentity,
): String? =
try {
// Use BC lightweight API directly — JCA provider registration is broken by R8
val privateKeyBytes = Base64.decode(identity.privateKeyPkcs8Base64, Base64.DEFAULT)
val pkInfo = org.bouncycastle.asn1.pkcs.PrivateKeyInfo.getInstance(privateKeyBytes)
val pkInfo =
org.bouncycastle.asn1.pkcs.PrivateKeyInfo
.getInstance(privateKeyBytes)
val parsed = pkInfo.parsePrivateKey()
val rawPrivate = org.bouncycastle.asn1.DEROctetString.getInstance(parsed).octets
val privateKey = org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(rawPrivate, 0)
val signer = org.bouncycastle.crypto.signers.Ed25519Signer()
val rawPrivate =
org.bouncycastle.asn1.DEROctetString
.getInstance(parsed)
.octets
val privateKey =
org.bouncycastle.crypto.params
.Ed25519PrivateKeyParameters(rawPrivate, 0)
val signer =
org.bouncycastle.crypto.signers
.Ed25519Signer()
signer.init(true, privateKey)
val payloadBytes = payload.toByteArray(Charsets.UTF_8)
signer.update(payloadBytes, 0, payloadBytes.size)
@@ -58,14 +73,21 @@ class DeviceIdentityStore(context: Context) {
android.util.Log.e("DeviceAuth", "signPayload FAILED: ${e.javaClass.simpleName}: ${e.message}", e)
null
}
}
fun verifySelfSignature(payload: String, signatureBase64Url: String, identity: DeviceIdentity): Boolean {
return try {
fun verifySelfSignature(
payload: String,
signatureBase64Url: String,
identity: DeviceIdentity,
): Boolean =
try {
val rawPublicKey = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT)
val pubKey = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(rawPublicKey, 0)
val pubKey =
org.bouncycastle.crypto.params
.Ed25519PublicKeyParameters(rawPublicKey, 0)
val sigBytes = base64UrlDecode(signatureBase64Url)
val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()
val verifier =
org.bouncycastle.crypto.signers
.Ed25519Signer()
verifier.init(false, pubKey)
val payloadBytes = payload.toByteArray(Charsets.UTF_8)
verifier.update(payloadBytes, 0, payloadBytes.size)
@@ -74,7 +96,6 @@ class DeviceIdentityStore(context: Context) {
android.util.Log.e("DeviceAuth", "self-verify exception: ${e.message}", e)
false
}
}
private fun base64UrlDecode(input: String): ByteArray {
val normalized = input.replace('-', '+').replace('_', '/')
@@ -82,18 +103,15 @@ class DeviceIdentityStore(context: Context) {
return Base64.decode(padded, Base64.DEFAULT)
}
fun publicKeyBase64Url(identity: DeviceIdentity): String? {
return try {
fun publicKeyBase64Url(identity: DeviceIdentity): String? =
try {
val raw = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT)
base64UrlEncode(raw)
} catch (_: Throwable) {
null
}
}
private fun load(): DeviceIdentity? {
return readIdentity(identityFile)
}
private fun load(): DeviceIdentity? = readIdentity(identityFile)
private fun readIdentity(file: File): DeviceIdentity? {
return try {
@@ -125,15 +143,22 @@ class DeviceIdentityStore(context: Context) {
private fun generate(): DeviceIdentity {
// Use BC lightweight API directly to avoid JCA provider issues with R8
val kpGen = org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator()
kpGen.init(org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters(java.security.SecureRandom()))
val kpGen =
org.bouncycastle.crypto.generators
.Ed25519KeyPairGenerator()
kpGen.init(
org.bouncycastle.crypto.params
.Ed25519KeyGenerationParameters(java.security.SecureRandom()),
)
val kp = kpGen.generateKeyPair()
val pubKey = kp.public as org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
val privKey = kp.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
val rawPublic = pubKey.encoded // 32 bytes
val rawPublic = pubKey.encoded // 32 bytes
val deviceId = sha256Hex(rawPublic)
// Encode private key as PKCS8 for storage
val privKeyInfo = org.bouncycastle.crypto.util.PrivateKeyInfoFactory.createPrivateKeyInfo(privKey)
val privKeyInfo =
org.bouncycastle.crypto.util.PrivateKeyInfoFactory
.createPrivateKeyInfo(privKey)
val pkcs8Bytes = privKeyInfo.encoded
return DeviceIdentity(
deviceId = deviceId,
@@ -143,14 +168,13 @@ class DeviceIdentityStore(context: Context) {
)
}
private fun deriveDeviceId(publicKeyRawBase64: String): String? {
return try {
private fun deriveDeviceId(publicKeyRawBase64: String): String? =
try {
val raw = Base64.decode(publicKeyRawBase64, Base64.DEFAULT)
sha256Hex(raw)
} catch (_: Throwable) {
null
}
}
private fun sha256Hex(data: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(data)
@@ -164,9 +188,11 @@ class DeviceIdentityStore(context: Context) {
return String(out)
}
private fun base64UrlEncode(data: ByteArray): String {
return Base64.encodeToString(data, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
private fun base64UrlEncode(data: ByteArray): String =
Base64.encodeToString(
data,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
)
companion object {
private val HEX = "0123456789abcdef".toCharArray()

View File

@@ -1,21 +1,17 @@
package ai.openclaw.app.gateway
import android.annotation.TargetApi
import android.content.Context
import android.net.ConnectivityManager
import android.net.DnsResolver
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.CancellationSignal
import android.util.Log
import java.io.IOException
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -32,19 +28,27 @@ import org.xbill.DNS.ExtendedResolver
import org.xbill.DNS.Message
import org.xbill.DNS.Name
import org.xbill.DNS.PTRRecord
import org.xbill.DNS.Record
import org.xbill.DNS.Rcode
import org.xbill.DNS.Record
import org.xbill.DNS.Resolver
import org.xbill.DNS.SRVRecord
import org.xbill.DNS.Section
import org.xbill.DNS.SimpleResolver
import org.xbill.DNS.TextParseException
import org.xbill.DNS.TXTRecord
import org.xbill.DNS.TextParseException
import org.xbill.DNS.Type
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Suppress("DEPRECATION")
class GatewayDiscovery(
context: Context,
private val scope: CoroutineScope,
@@ -66,15 +70,38 @@ class GatewayDiscovery(
private var unicastJob: Job? = null
private val dnsExecutor: Executor = Executors.newCachedThreadPool()
private val availableNetworks = ConcurrentHashMap.newKeySet<Network>()
private val serviceInfoCallbacks = ConcurrentHashMap<String, Any>()
@Volatile private var lastWideAreaRcode: Int? = null
@Volatile private var lastWideAreaCount: Int = 0
private val networkCallback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
availableNetworks.add(network)
}
override fun onLost(network: Network) {
availableNetworks.remove(network)
}
}
private val discoveryListener =
object : NsdManager.DiscoveryListener {
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
override fun onStartDiscoveryFailed(
serviceType: String,
errorCode: Int,
) {}
override fun onStopDiscoveryFailed(
serviceType: String,
errorCode: Int,
) {}
override fun onDiscoveryStarted(serviceType: String) {}
override fun onDiscoveryStopped(serviceType: String) {}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
@@ -86,17 +113,29 @@ class GatewayDiscovery(
val serviceName = BonjourEscapes.decode(serviceInfo.serviceName)
val id = stableId(serviceName, "local.")
localById.remove(id)
unregisterServiceInfoCallback(id)
publish()
}
}
init {
startNetworkTracking()
startLocalDiscovery()
if (!wideAreaDomain.isNullOrBlank()) {
startUnicastDiscovery(wideAreaDomain)
}
}
private fun startNetworkTracking() {
val cm = connectivity ?: return
cm.activeNetwork?.let(availableNetworks::add)
try {
cm.registerNetworkCallback(NetworkRequest.Builder().build(), networkCallback)
} catch (_: Throwable) {
// ignore (best-effort)
}
}
private fun startLocalDiscovery() {
try {
nsd.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
@@ -128,43 +167,124 @@ class GatewayDiscovery(
}
private fun resolve(serviceInfo: NsdServiceInfo) {
nsd.resolveService(
serviceInfo,
object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
resolveWithServiceInfoCallback(serviceInfo)
} else {
resolveLegacy(serviceInfo)
}
}
override fun onServiceResolved(resolved: NsdServiceInfo) {
val host = resolved.host?.hostAddress ?: return
val port = resolved.port
if (port <= 0) return
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun resolveWithServiceInfoCallback(serviceInfo: NsdServiceInfo) {
val serviceName = BonjourEscapes.decode(serviceInfo.serviceName)
val id = stableId(serviceName, "local.")
if (serviceInfoCallbacks.containsKey(id)) return
val rawServiceName = resolved.serviceName
val serviceName = BonjourEscapes.decode(rawServiceName)
val displayName = BonjourEscapes.decode(txt(resolved, "displayName") ?: serviceName)
val lanHost = txt(resolved, "lanHost")
val tailnetDns = txt(resolved, "tailnetDns")
val gatewayPort = txtInt(resolved, "gatewayPort")
val canvasPort = txtInt(resolved, "canvasPort")
val tlsEnabled = txtBool(resolved, "gatewayTls")
val tlsFingerprint = txt(resolved, "gatewayTlsSha256")
val id = stableId(serviceName, "local.")
localById[id] =
GatewayEndpoint(
stableId = id,
name = displayName,
host = host,
port = port,
lanHost = lanHost,
tailnetDns = tailnetDns,
gatewayPort = gatewayPort,
canvasPort = canvasPort,
tlsEnabled = tlsEnabled,
tlsFingerprintSha256 = tlsFingerprint,
)
val callback =
object : NsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
serviceInfoCallbacks.remove(id, this)
}
override fun onServiceInfoCallbackUnregistered() {
serviceInfoCallbacks.remove(id, this)
}
override fun onServiceLost() {
localById.remove(id)
publish()
}
},
)
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
upsertResolvedService(serviceInfo)
}
}
serviceInfoCallbacks[id] = callback
try {
nsd.registerServiceInfoCallback(serviceInfo, dnsExecutor, callback)
} catch (_: Throwable) {
serviceInfoCallbacks.remove(id, callback)
}
}
private fun unregisterServiceInfoCallback(id: String) {
val callback = serviceInfoCallbacks.remove(id) ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return
try {
nsd.unregisterServiceInfoCallback(callback as NsdManager.ServiceInfoCallback)
} catch (_: Throwable) {
// ignore (best-effort)
}
}
private fun resolveLegacy(serviceInfo: NsdServiceInfo) {
val listener =
object : NsdManager.ResolveListener {
override fun onResolveFailed(
serviceInfo: NsdServiceInfo,
errorCode: Int,
) {}
override fun onServiceResolved(resolved: NsdServiceInfo) {
upsertResolvedService(resolved)
}
}
try {
NsdManager::class.java
.getMethod("resolveService", NsdServiceInfo::class.java, NsdManager.ResolveListener::class.java)
.invoke(nsd, serviceInfo, listener)
} catch (_: Throwable) {
// ignore (best-effort)
}
}
private fun upsertResolvedService(resolved: NsdServiceInfo) {
val host = resolvedHostAddress(resolved) ?: return
val port = resolved.port
if (port <= 0) return
val rawServiceName = resolved.serviceName
val serviceName = BonjourEscapes.decode(rawServiceName)
val displayName = BonjourEscapes.decode(txt(resolved, "displayName") ?: serviceName)
val lanHost = txt(resolved, "lanHost")
val tailnetDns = txt(resolved, "tailnetDns")
val gatewayPort = txtInt(resolved, "gatewayPort")
val canvasPort = txtInt(resolved, "canvasPort")
val tlsEnabled = txtBool(resolved, "gatewayTls")
val tlsFingerprint = txt(resolved, "gatewayTlsSha256")
val id = stableId(serviceName, "local.")
localById[id] =
GatewayEndpoint(
stableId = id,
name = displayName,
host = host,
port = port,
lanHost = lanHost,
tailnetDns = tailnetDns,
gatewayPort = gatewayPort,
canvasPort = canvasPort,
tlsEnabled = tlsEnabled,
tlsFingerprintSha256 = tlsFingerprint,
)
publish()
}
private fun resolvedHostAddress(resolved: NsdServiceInfo): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return resolved.hostAddresses.firstOrNull()?.hostAddress
}
return legacyHostAddress(resolved)
}
private fun legacyHostAddress(resolved: NsdServiceInfo): String? {
return try {
val host = NsdServiceInfo::class.java.getMethod("getHost").invoke(resolved) as? InetAddress
host?.hostAddress
} catch (_: Throwable) {
null
}
}
private fun publish() {
@@ -193,15 +313,17 @@ class GatewayDiscovery(
}
}
private fun stableId(serviceName: String, domain: String): String {
return "${serviceType}|${domain}|${normalizeName(serviceName)}"
}
private fun stableId(
serviceName: String,
domain: String,
): String = "$serviceType|$domain|${normalizeName(serviceName)}"
private fun normalizeName(raw: String): String {
return raw.trim().split(Regex("\\s+")).joinToString(" ")
}
private fun normalizeName(raw: String): String = raw.trim().split(Regex("\\s+")).joinToString(" ")
private fun txt(info: NsdServiceInfo, key: String): String? {
private fun txt(
info: NsdServiceInfo,
key: String,
): String? {
val bytes = info.attributes[key] ?: return null
return try {
String(bytes, Charsets.UTF_8).trim().ifEmpty { null }
@@ -210,17 +332,21 @@ class GatewayDiscovery(
}
}
private fun txtInt(info: NsdServiceInfo, key: String): Int? {
return txt(info, key)?.toIntOrNull()
}
private fun txtInt(
info: NsdServiceInfo,
key: String,
): Int? = txt(info, key)?.toIntOrNull()
private fun txtBool(info: NsdServiceInfo, key: String): Boolean {
private fun txtBool(
info: NsdServiceInfo,
key: String,
): Boolean {
val raw = txt(info, key)?.trim()?.lowercase() ?: return false
return raw == "1" || raw == "true" || raw == "yes"
}
private suspend fun refreshUnicast(domain: String) {
val ptrName = "${serviceType}${domain}"
val ptrName = "${serviceType}$domain"
val ptrMsg = lookupUnicastMessage(ptrName, Type.PTR) ?: return
val ptrRecords = records(ptrMsg, Section.ANSWER).mapNotNull { it as? PTRRecord }
@@ -293,8 +419,11 @@ class GatewayDiscovery(
}
}
private fun decodeInstanceName(instanceFqdn: String, domain: String): String {
val suffix = "${serviceType}${domain}"
private fun decodeInstanceName(
instanceFqdn: String,
domain: String,
): String {
val suffix = "${serviceType}$domain"
val withoutSuffix =
if (instanceFqdn.endsWith(suffix)) {
instanceFqdn.removeSuffix(suffix)
@@ -304,11 +433,12 @@ class GatewayDiscovery(
return normalizeName(stripTrailingDot(withoutSuffix))
}
private fun stripTrailingDot(raw: String): String {
return raw.removeSuffix(".")
}
private fun stripTrailingDot(raw: String): String = raw.removeSuffix(".")
private suspend fun lookupUnicastMessage(name: String, type: Int): Message? {
private suspend fun lookupUnicastMessage(
name: String,
type: Int,
): Message? {
val query =
try {
Message.newQuery(
@@ -350,15 +480,17 @@ class GatewayDiscovery(
}
}
private fun records(msg: Message?, section: Int): List<Record> {
return msg?.getSection(section).orEmpty()
}
private fun records(
msg: Message?,
section: Int,
): List<Record> = msg?.getSection(section).orEmpty()
private fun keyName(raw: String): String {
return raw.trim().lowercase()
}
private fun keyName(raw: String): String = raw.trim().lowercase()
private fun recordsByName(msg: Message, section: Int): Map<String, List<Record>> {
private fun recordsByName(
msg: Message,
section: Int,
): Map<String, List<Record>> {
val next = LinkedHashMap<String, MutableList<Record>>()
for (r in records(msg, section)) {
val name = r.name?.toString() ?: continue
@@ -367,7 +499,11 @@ class GatewayDiscovery(
return next
}
private fun recordByName(msg: Message, fqdn: String, type: Int): Record? {
private fun recordByName(
msg: Message,
fqdn: String,
type: Int,
): Record? {
val key = keyName(fqdn)
val byNameAnswer = recordsByName(msg, Section.ANSWER)
val fromAnswer = byNameAnswer[key].orEmpty().firstOrNull { it.type == type }
@@ -377,7 +513,10 @@ class GatewayDiscovery(
return byNameAdditional[key].orEmpty().firstOrNull { it.type == type }
}
private fun resolveHostFromMessage(msg: Message?, hostname: String): String? {
private fun resolveHostFromMessage(
msg: Message?,
hostname: String,
): String? {
val m = msg ?: return null
val key = keyName(hostname)
val additional = recordsByName(m, Section.ADDITIONAL)[key].orEmpty()
@@ -390,7 +529,7 @@ class GatewayDiscovery(
val cm = connectivity ?: return null
// Prefer VPN (Tailscale) when present; otherwise use the active network.
cm.allNetworks.firstOrNull { n ->
trackedNetworks(cm).firstOrNull { n ->
val caps = cm.getNetworkCapabilities(n) ?: return@firstOrNull false
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}?.let { return it }
@@ -398,12 +537,19 @@ class GatewayDiscovery(
return cm.activeNetwork
}
private fun trackedNetworks(cm: ConnectivityManager): List<Network> {
return buildList {
cm.activeNetwork?.let(::add)
addAll(availableNetworks)
}.distinct()
}
private fun createDirectResolver(): Resolver? {
val cm = connectivity ?: return null
val candidateNetworks =
buildList {
cm.allNetworks
trackedNetworks(cm)
.firstOrNull { n ->
val caps = cm.getNetworkCapabilities(n) ?: return@firstOrNull false
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
@@ -416,8 +562,7 @@ class GatewayDiscovery(
.asSequence()
.flatMap { n ->
cm.getLinkProperties(n)?.dnsServers?.asSequence() ?: emptySequence()
}
.distinctBy { it.hostAddress ?: it.toString() }
}.distinctBy { it.hostAddress ?: it.toString() }
.toList()
if (servers.isEmpty()) return null
@@ -440,7 +585,10 @@ class GatewayDiscovery(
}
}
private suspend fun rawQuery(network: android.net.Network?, wireQuery: ByteArray): ByteArray =
private suspend fun rawQuery(
network: android.net.Network?,
wireQuery: ByteArray,
): ByteArray =
suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
@@ -452,7 +600,10 @@ class GatewayDiscovery(
dnsExecutor,
signal,
object : DnsResolver.Callback<ByteArray> {
override fun onAnswer(answer: ByteArray, rcode: Int) {
override fun onAnswer(
answer: ByteArray,
rcode: Int,
) {
cont.resume(answer)
}
@@ -463,7 +614,10 @@ class GatewayDiscovery(
)
}
private fun txtValue(records: List<TXTRecord>, key: String): String? {
private fun txtValue(
records: List<TXTRecord>,
key: String,
): String? {
val prefix = "$key="
for (r in records) {
val strings: List<String> =
@@ -482,11 +636,15 @@ class GatewayDiscovery(
return null
}
private fun txtIntValue(records: List<TXTRecord>, key: String): Int? {
return txtValue(records, key)?.toIntOrNull()
}
private fun txtIntValue(
records: List<TXTRecord>,
key: String,
): Int? = txtValue(records, key)?.toIntOrNull()
private fun txtBoolValue(records: List<TXTRecord>, key: String): Boolean {
private fun txtBoolValue(
records: List<TXTRecord>,
key: String,
): Boolean {
val raw = txtValue(records, key)?.trim()?.lowercase() ?: return false
return raw == "1" || raw == "true" || raw == "yes"
}

View File

@@ -13,7 +13,10 @@ data class GatewayEndpoint(
val tlsFingerprintSha256: String? = null,
) {
companion object {
fun manual(host: String, port: Int): GatewayEndpoint =
fun manual(
host: String,
port: Int,
): GatewayEndpoint =
GatewayEndpoint(
stableId = "manual|${host.lowercase()}|$port",
name = "$host:$port",

View File

@@ -1,10 +1,6 @@
package ai.openclaw.app.gateway
import android.util.Log
import java.util.Locale
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -30,6 +26,10 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.Locale
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
data class GatewayClientInfo(
val id: String,
@@ -77,8 +77,9 @@ private data class SelectedConnectAuth(
val attemptedDeviceTokenRetry: Boolean,
)
private class GatewayConnectFailure(val gatewayError: GatewaySession.ErrorShape) :
IllegalStateException(gatewayError.message)
private class GatewayConnectFailure(
val gatewayError: GatewaySession.ErrorShape,
) : IllegalStateException(gatewayError.message)
class GatewaySession(
private val scope: CoroutineScope,
@@ -103,11 +104,18 @@ class GatewaySession(
val timeoutMs: Long?,
)
data class InvokeResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) {
data class InvokeResult(
val ok: Boolean,
val payloadJson: String?,
val error: ErrorShape?,
) {
companion object {
fun ok(payloadJson: String?) = InvokeResult(ok = true, payloadJson = payloadJson, error = null)
fun error(code: String, message: String) =
InvokeResult(ok = false, payloadJson = null, error = ErrorShape(code = code, message = message))
fun error(
code: String,
message: String,
) = InvokeResult(ok = false, payloadJson = null, error = ErrorShape(code = code, message = message))
}
}
@@ -117,13 +125,18 @@ class GatewaySession(
val details: GatewayConnectErrorDetails? = null,
)
data class RpcResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?)
data class RpcResult(
val ok: Boolean,
val payloadJson: String?,
val error: ErrorShape?,
)
private val json = Json { ignoreUnknownKeys = true }
private val writeLock = Mutex()
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
@Volatile private var canvasHostUrl: String? = null
@Volatile private var mainSessionKey: String? = null
private data class DesiredConnection(
@@ -137,9 +150,13 @@ class GatewaySession(
private var desired: DesiredConnection? = null
private var job: Job? = null
@Volatile private var currentConnection: Connection? = null
@Volatile private var pendingDeviceTokenRetry = false
@Volatile private var deviceTokenRetryBudgetUsed = false
@Volatile private var reconnectPausedForAuthFailure = false
fun connect(
@@ -180,32 +197,78 @@ class GatewaySession(
}
fun currentCanvasHostUrl(): String? = canvasHostUrl
fun currentMainSessionKey(): String? = mainSessionKey
suspend fun sendNodeEvent(event: String, payloadJson: String?): Boolean {
suspend fun sendNodeEvent(
event: String,
payloadJson: String?,
): Boolean {
val conn = currentConnection ?: return false
val params =
buildJsonObject {
put("event", JsonPrimitive(event))
put("payloadJSON", JsonPrimitive(payloadJson ?: "{}"))
}
try {
conn.request("node.event", params, timeoutMs = 8_000)
return true
return try {
conn.request(
"node.event",
buildNodeEventParams(event = event, payloadJson = payloadJson),
timeoutMs = 8_000,
)
true
} catch (err: Throwable) {
Log.w("OpenClawGateway", "node.event failed: ${err.message ?: err::class.java.simpleName}")
return false
Log.w("OpenClawGateway", "node.event failed: ${err::class.java.simpleName}")
false
}
}
suspend fun request(method: String, paramsJson: String?, timeoutMs: Long = 15_000): String {
suspend fun sendNodeEventDetailed(
event: String,
payloadJson: String?,
timeoutMs: Long = 8_000,
): RpcResult {
val conn =
currentConnection
?: return RpcResult(
ok = false,
payloadJson = null,
error = ErrorShape("UNAVAILABLE", "not connected"),
)
val params = buildNodeEventParams(event = event, payloadJson = payloadJson)
try {
val res = conn.request("node.event", params, timeoutMs = timeoutMs)
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
} catch (err: Throwable) {
Log.w("OpenClawGateway", "node.event failed: ${err::class.java.simpleName}")
return RpcResult(
ok = false,
payloadJson = null,
error = ErrorShape("UNAVAILABLE", "node.event failed"),
)
}
}
private fun buildNodeEventParams(
event: String,
payloadJson: String?,
): JsonObject =
buildJsonObject {
put("event", JsonPrimitive(event))
put("payloadJSON", JsonPrimitive(payloadJson ?: "{}"))
}
suspend fun request(
method: String,
paramsJson: String?,
timeoutMs: Long = 15_000,
): String {
val res = requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
if (res.ok) return res.payloadJson ?: ""
val err = res.error
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
}
suspend fun requestDetailed(method: String, paramsJson: String?, timeoutMs: Long = 15_000): RpcResult {
suspend fun requestDetailed(
method: String,
paramsJson: String?,
timeoutMs: Long = 15_000,
): RpcResult {
val conn = currentConnection ?: throw IllegalStateException("not connected")
val params =
if (paramsJson.isNullOrBlank()) {
@@ -239,7 +302,12 @@ class GatewaySession(
return false
}
val payloadObj = response.payloadJson?.let(::parseJsonOrNull)?.asObjectOrNull()
val refreshedCapability = payloadObj?.get("canvasCapability").asStringOrNull()?.trim().orEmpty()
val refreshedCapability =
payloadObj
?.get("canvasCapability")
.asStringOrNull()
?.trim()
.orEmpty()
if (refreshedCapability.isEmpty()) {
Log.w("OpenClawGateway", "node.canvas.capability.refresh missing canvasCapability")
return false
@@ -258,7 +326,12 @@ class GatewaySession(
return true
}
private data class RpcResponse(val id: String, val ok: Boolean, val payloadJson: String?, val error: ErrorShape?)
private data class RpcResponse(
val id: String,
val ok: Boolean,
val payloadJson: String?,
val error: ErrorShape?,
)
private inner class Connection(
private val endpoint: GatewayEndpoint,
@@ -289,7 +362,11 @@ class GatewaySession(
}
}
suspend fun request(method: String, params: JsonElement?, timeoutMs: Long): RpcResponse {
suspend fun request(
method: String,
params: JsonElement?,
timeoutMs: Long,
): RpcResponse {
val id = UUID.randomUUID().toString()
val deferred = CompletableDeferred<RpcResponse>()
pending[id] = deferred
@@ -327,13 +404,16 @@ class GatewaySession(
}
private fun buildClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
val tlsConfig = buildGatewayTlsConfig(tls) { fingerprint ->
onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint)
}
val builder =
OkHttpClient
.Builder()
.writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
val tlsConfig =
buildGatewayTlsConfig(tls) { fingerprint ->
onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint)
}
if (tlsConfig != null) {
builder.sslSocketFactory(tlsConfig.sslSocketFactory, tlsConfig.trustManager)
builder.hostnameVerifier(tlsConfig.hostnameVerifier)
@@ -342,7 +422,10 @@ class GatewaySession(
}
private inner class Listener : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
override fun onOpen(
webSocket: WebSocket,
response: Response,
) {
scope.launch {
try {
val nonce = awaitConnectNonce()
@@ -354,11 +437,18 @@ class GatewaySession(
}
}
override fun onMessage(webSocket: WebSocket, text: String) {
override fun onMessage(
webSocket: WebSocket,
text: String,
) {
scope.launch { handleMessage(text) }
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?,
) {
if (!connectDeferred.isCompleted) {
connectDeferred.completeExceptionally(t)
}
@@ -369,7 +459,11 @@ class GatewaySession(
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
override fun onClosed(
webSocket: WebSocket,
code: Int,
reason: String,
) {
if (!connectDeferred.isCompleted) {
connectDeferred.completeExceptionally(IllegalStateException("Gateway closed: $reason"))
}
@@ -420,7 +514,7 @@ class GatewaySession(
deviceTokenRetryBudgetUsed = true
} else if (
selectedAuth.attemptedDeviceTokenRetry &&
shouldClearStoredDeviceTokenAfterRetry(error)
shouldClearStoredDeviceTokenAfterRetry(error)
) {
deviceAuthStore.clearToken(identity.deviceId, options.role)
}
@@ -436,8 +530,11 @@ class GatewaySession(
return tls != null
}
private fun filteredBootstrapHandoffScopes(role: String, scopes: List<String>): List<String>? {
return when (role.trim()) {
private fun filteredBootstrapHandoffScopes(
role: String,
scopes: List<String>,
): List<String>? =
when (role.trim()) {
"node" -> emptyList()
"operator" -> {
val allowedOperatorScopes =
@@ -451,7 +548,6 @@ class GatewaySession(
}
else -> null
}
}
private fun persistBootstrapHandoffToken(
deviceId: String,
@@ -493,20 +589,25 @@ class GatewaySession(
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
val authScopes =
authObj?.get("scopes").asArrayOrNull()
authObj
?.get("scopes")
.asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!deviceToken.isNullOrBlank()) {
persistIssuedDeviceToken(authSource, deviceId, authRole, deviceToken, authScopes)
}
if (shouldPersistBootstrapHandoffTokens(authSource)) {
authObj?.get("deviceTokens").asArrayOrNull()
authObj
?.get("deviceTokens")
.asArrayOrNull()
?.mapNotNull { it.asObjectOrNull() }
?.forEach { tokenEntry ->
val handoffToken = tokenEntry["deviceToken"].asStringOrNull()
val handoffRole = tokenEntry["role"].asStringOrNull()
val handoffScopes =
tokenEntry["scopes"].asArrayOrNull()
tokenEntry["scopes"]
.asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!handoffToken.isNullOrBlank() && !handoffRole.isNullOrBlank()) {
@@ -517,8 +618,10 @@ class GatewaySession(
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null)
val sessionDefaults =
obj["snapshot"].asObjectOrNull()
?.get("sessionDefaults").asObjectOrNull()
obj["snapshot"]
.asObjectOrNull()
?.get("sessionDefaults")
.asObjectOrNull()
mainSessionKey = sessionDefaults?.get("mainSessionKey").asStringOrNull()
onConnected(serverName, remoteAddress, mainSessionKey)
}
@@ -665,13 +768,12 @@ class GatewaySession(
onEvent(event, payloadJson)
}
private suspend fun awaitConnectNonce(): String {
return try {
private suspend fun awaitConnectNonce(): String =
try {
withTimeout(2_000) { connectNonceDeferred.await() }
} catch (err: Throwable) {
throw IllegalStateException("connect challenge timeout", err)
}
}
private fun extractConnectNonce(payloadJson: String?): String? {
if (payloadJson.isNullOrBlank()) return null
@@ -780,7 +882,7 @@ class GatewaySession(
onDisconnected("Gateway error: ${err.message ?: err::class.java.simpleName}")
if (
err is GatewayConnectFailure &&
shouldPauseReconnectAfterAuthFailure(err.gatewayError)
shouldPauseReconnectAfterAuthFailure(err.gatewayError)
) {
reconnectPausedForAuthFailure = true
continue
@@ -791,26 +893,27 @@ class GatewaySession(
}
}
private suspend fun connectOnce(target: DesiredConnection) = withContext(Dispatchers.IO) {
val conn =
Connection(
target.endpoint,
target.token,
target.bootstrapToken,
target.password,
target.options,
target.tls,
)
currentConnection = conn
try {
conn.connect()
conn.awaitClose()
} finally {
currentConnection = null
canvasHostUrl = null
mainSessionKey = null
private suspend fun connectOnce(target: DesiredConnection) =
withContext(Dispatchers.IO) {
val conn =
Connection(
target.endpoint,
target.token,
target.bootstrapToken,
target.password,
target.options,
target.tls,
)
currentConnection = conn
try {
conn.connect()
conn.awaitClose()
} finally {
currentConnection = null
canvasHostUrl = null
mainSessionKey = null
}
}
}
private fun normalizeCanvasHostUrl(
raw: String?,
@@ -821,7 +924,12 @@ class GatewaySession(
val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { java.net.URI(it) }.getOrNull() }
val host = parsed?.host?.trim().orEmpty()
val port = parsed?.port ?: -1
val scheme = parsed?.scheme?.trim().orEmpty().ifBlank { "http" }
val scheme =
parsed
?.scheme
?.trim()
.orEmpty()
.ifBlank { "http" }
val suffix = buildUrlSuffix(parsed)
// If raw URL is a non-loopback address and this connection uses TLS,
@@ -833,7 +941,7 @@ class GatewaySession(
!scheme.equals("https", ignoreCase = true) ||
(port > 0 && port != endpoint.port) ||
(port <= 0 && endpoint.port != 443)
)
)
if (needsTlsRewrite) {
return buildCanvasUrl(host = host, scheme = "https", port = endpoint.port, suffix = suffix)
}
@@ -853,7 +961,12 @@ class GatewaySession(
return buildCanvasUrl(host = fallbackHost, scheme = fallbackScheme, port = fallbackPort, suffix = suffix)
}
private fun buildCanvasUrl(host: String, scheme: String, port: Int, suffix: String): String {
private fun buildCanvasUrl(
host: String,
scheme: String,
port: Int,
suffix: String,
): String {
val loweredScheme = scheme.lowercase()
val formattedHost = formatGatewayAuthorityHost(host)
val portSuffix = if ((loweredScheme == "https" && port == 443) || (loweredScheme == "http" && port == 80)) "" else ":$port"
@@ -886,7 +999,7 @@ class GatewaySession(
explicitGatewayToken
?: if (
explicitPassword == null &&
(explicitBootstrapToken == null || storedToken != null)
(explicitBootstrapToken == null || storedToken != null)
) {
storedToken
} else {
@@ -933,8 +1046,8 @@ class GatewaySession(
detailCode == "AUTH_TOKEN_MISMATCH"
}
private fun shouldPauseReconnectAfterAuthFailure(error: ErrorShape): Boolean {
return when (error.details?.code) {
private fun shouldPauseReconnectAfterAuthFailure(error: ErrorShape): Boolean =
when (error.details?.code) {
"AUTH_TOKEN_MISSING",
"AUTH_BOOTSTRAP_TOKEN_INVALID",
"AUTH_PASSWORD_MISSING",
@@ -942,15 +1055,13 @@ class GatewaySession(
"AUTH_RATE_LIMITED",
"PAIRING_REQUIRED",
"CONTROL_UI_DEVICE_IDENTITY_REQUIRED",
"DEVICE_IDENTITY_REQUIRED" -> true
"DEVICE_IDENTITY_REQUIRED",
-> true
"AUTH_TOKEN_MISMATCH" -> deviceTokenRetryBudgetUsed && !pendingDeviceTokenRetry
else -> false
}
}
private fun shouldClearStoredDeviceTokenAfterRetry(error: ErrorShape): Boolean {
return error.details?.code == "AUTH_DEVICE_TOKEN_MISMATCH"
}
private fun shouldClearStoredDeviceTokenAfterRetry(error: ErrorShape): Boolean = error.details?.code == "AUTH_DEVICE_TOKEN_MISMATCH"
private fun isTrustedDeviceRetryEndpoint(
endpoint: GatewayEndpoint,
@@ -963,18 +1074,23 @@ class GatewaySession(
}
}
internal fun buildGatewayWebSocketUrl(host: String, port: Int, useTls: Boolean): String {
internal fun buildGatewayWebSocketUrl(
host: String,
port: Int,
useTls: Boolean,
): String {
val scheme = if (useTls) "wss" else "ws"
return "$scheme://${formatGatewayAuthority(host, port)}"
}
internal fun formatGatewayAuthority(host: String, port: Int): String {
return "${formatGatewayAuthorityHost(host)}:$port"
}
internal fun formatGatewayAuthority(
host: String,
port: Int,
): String = "${formatGatewayAuthorityHost(host)}:$port"
private fun formatGatewayAuthorityHost(host: String): String {
val normalizedHost = host.trim().trim('[', ']')
return if (normalizedHost.contains(":")) "[${normalizedHost}]" else normalizedHost
return if (normalizedHost.contains(":")) "[$normalizedHost]" else normalizedHost
}
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject

View File

@@ -13,14 +13,15 @@ import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.Locale
import javax.net.ssl.HttpsURLConnection
import java.util.concurrent.atomic.AtomicReference
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SNIHostName
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLException
import javax.net.ssl.SSLParameters
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.SNIHostName
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
@@ -54,14 +55,21 @@ fun buildGatewayTlsConfig(
if (params == null) return null
val expected = params.expectedFingerprint?.let(::normalizeFingerprint)
val defaultTrust = defaultTrustManager()
@SuppressLint("CustomX509TrustManager")
val trustManager =
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String,
) {
defaultTrust.checkClientTrusted(chain, authType)
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String,
) {
if (chain.isEmpty()) throw CertificateException("empty certificate chain")
val fingerprint = sha256Hex(chain[0].encoded)
if (expected != null) {
@@ -106,18 +114,29 @@ suspend fun probeGatewayTlsFingerprint(
if (port !in 1..65535) return GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE)
return withContext(Dispatchers.IO) {
val trustAll =
@SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager")
val fingerprintRef = AtomicReference<String?>(null)
val probeTrustManager =
@SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String,
): Unit = throw CertificateException("gateway TLS probe does not accept client certificates")
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String,
) {
if (chain.isEmpty()) throw CertificateException("empty certificate chain")
fingerprintRef.set(sha256Hex(chain[0].encoded))
throw CertificateException("gateway TLS probe captured fingerprint")
}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val context = SSLContext.getInstance("TLS")
context.init(null, arrayOf(trustAll), SecureRandom())
context.init(null, arrayOf(probeTrustManager), SecureRandom())
val socket = (context.socketFactory.createSocket() as SSLSocket)
try {
@@ -141,13 +160,16 @@ suspend fun probeGatewayTlsFingerprint(
?: return@withContext GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.TLS_UNAVAILABLE)
GatewayTlsProbeResult(fingerprintSha256 = sha256Hex(cert.encoded))
} catch (err: Throwable) {
fingerprintRef.get()?.let { return@withContext GatewayTlsProbeResult(fingerprintSha256 = it) }
val failure =
when (err) {
is SSLException,
is EOFException -> GatewayTlsProbeFailure.TLS_UNAVAILABLE
is EOFException,
-> GatewayTlsProbeFailure.TLS_UNAVAILABLE
is ConnectException,
is SocketTimeoutException,
is UnknownHostException -> GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE
is UnknownHostException,
-> GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE
else -> GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE
}
GatewayTlsProbeResult(failure = failure)
@@ -179,7 +201,9 @@ private fun sha256Hex(data: ByteArray): String {
}
private fun normalizeFingerprint(raw: String): String {
val stripped = raw.trim()
.replace(Regex("^sha-?256\\s*:?\\s*", RegexOption.IGNORE_CASE), "")
val stripped =
raw
.trim()
.replace(Regex("^sha-?256\\s*:?\\s*", RegexOption.IGNORE_CASE), "")
return stripped.lowercase(Locale.US).filter { it in '0'..'9' || it in 'a'..'f' }
}

View File

@@ -1,6 +1,5 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.delay
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
@@ -13,12 +12,11 @@ class A2UIHandler(
private val getNodeCanvasHostUrl: () -> String?,
private val getOperatorCanvasHostUrl: () -> String?,
) {
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
return CanvasActionTrust.isTrustedCanvasActionUrl(
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean =
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = rawUrl,
trustedA2uiUrls = listOfNotNull(resolveA2uiHostUrl()),
)
}
fun resolveA2uiHostUrl(): String? {
val nodeRaw = getNodeCanvasHostUrl()?.trim().orEmpty()
@@ -26,7 +24,7 @@ class A2UIHandler(
val raw = if (nodeRaw.isNotBlank()) nodeRaw else operatorRaw
if (raw.isBlank()) return null
val base = raw.trimEnd('/')
return "${base}/__openclaw__/a2ui/?platform=android"
return "$base/__openclaw__/a2ui/?platform=android"
}
suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
@@ -50,7 +48,10 @@ class A2UIHandler(
return false
}
fun decodeA2uiMessages(command: String, paramsJson: String?): String {
fun decodeA2uiMessages(
command: String,
paramsJson: String?,
): String {
val raw = paramsJson?.trim().orEmpty()
if (raw.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: paramsJSON required")
@@ -76,8 +77,7 @@ class A2UIHandler(
?: throw IllegalArgumentException("A2UI JSONL line ${idx + 1}: expected a JSON object")
validateA2uiV0_8(msg, idx + 1)
msg
}
.toList()
}.toList()
return JsonArray(messages).toString()
}
@@ -86,14 +86,17 @@ class A2UIHandler(
arr.mapIndexed { idx, el ->
val msg =
el as? JsonObject
?: throw IllegalArgumentException("A2UI messages[${idx}]: expected a JSON object")
?: throw IllegalArgumentException("A2UI messages[$idx]: expected a JSON object")
validateA2uiV0_8(msg, idx + 1)
msg
}
return JsonArray(out).toString()
}
private fun validateA2uiV0_8(msg: JsonObject, lineNumber: Int) {
private fun validateA2uiV0_8(
msg: JsonObject,
lineNumber: Int,
) {
if (msg.containsKey("createSurface")) {
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: looks like A2UI v0.9 (`createSurface`). Canvas supports v0.8 messages only.",
@@ -135,19 +138,18 @@ class A2UIHandler(
})()
"""
fun a2uiApplyMessagesJS(messagesJson: String): String {
return """
(() => {
try {
const host = globalThis.openclawA2UI;
if (!host) return { ok: false, error: "missing openclawA2UI" };
const messages = $messagesJson;
return host.applyMessages(messages);
} catch (e) {
return { ok: false, error: String(e?.message ?? e) };
}
})()
fun a2uiApplyMessagesJS(messagesJson: String): String =
"""
(() => {
try {
const host = globalThis.openclawA2UI;
if (!host) return { ok: false, error: "missing openclawA2UI" };
const messages = $messagesJson;
return host.applyMessages(messages);
} catch (e) {
return { ok: false, error: String(e?.message ?? e) };
}
})()
""".trimIndent()
}
}
}

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.content.ContentResolver
import android.content.ContentUris
@@ -7,16 +8,15 @@ import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.TimeZone
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.TimeZone
private const val DEFAULT_CALENDAR_LIMIT = 50
@@ -52,23 +52,30 @@ internal interface CalendarDataSource {
fun hasWritePermission(context: Context): Boolean
fun events(context: Context, request: CalendarEventsRequest): List<CalendarEventRecord>
fun events(
context: Context,
request: CalendarEventsRequest,
): List<CalendarEventRecord>
fun add(context: Context, request: CalendarAddRequest): CalendarEventRecord
fun add(
context: Context,
request: CalendarAddRequest,
): CalendarEventRecord
}
private object SystemCalendarDataSource : CalendarDataSource {
override fun hasReadPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) ==
override fun hasReadPermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun hasWritePermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) ==
override fun hasWritePermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun events(context: Context, request: CalendarEventsRequest): List<CalendarEventRecord> {
override fun events(
context: Context,
request: CalendarEventsRequest,
): List<CalendarEventRecord> {
val resolver = context.contentResolver
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, request.startMs)
@@ -89,7 +96,12 @@ private object SystemCalendarDataSource : CalendarDataSource {
val out = mutableListOf<CalendarEventRecord>()
while (cursor.moveToNext() && out.size < request.limit) {
val id = cursor.getLong(0)
val title = cursor.getString(1)?.trim().orEmpty().ifEmpty { "(untitled)" }
val title =
cursor
.getString(1)
?.trim()
.orEmpty()
.ifEmpty { "(untitled)" }
val beginMs = cursor.getLong(2)
val endMs = cursor.getLong(3)
val isAllDay = cursor.getInt(4) == 1
@@ -110,7 +122,10 @@ private object SystemCalendarDataSource : CalendarDataSource {
}
}
override fun add(context: Context, request: CalendarAddRequest): CalendarEventRecord {
override fun add(
context: Context,
request: CalendarAddRequest,
): CalendarEventRecord {
val resolver = context.contentResolver
val resolvedCalendarId = resolveCalendarId(resolver, request.calendarId, request.calendarTitle)
val values =
@@ -124,10 +139,12 @@ private object SystemCalendarDataSource : CalendarDataSource {
request.location?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
request.notes?.let { put(CalendarContract.Events.DESCRIPTION, it) }
}
val uri = resolver.insert(CalendarContract.Events.CONTENT_URI, values)
?: throw IllegalStateException("calendar insert failed")
val eventId = uri.lastPathSegment?.toLongOrNull()
?: throw IllegalStateException("calendar insert failed")
val uri =
resolver.insert(CalendarContract.Events.CONTENT_URI, values)
?: throw IllegalStateException("calendar insert failed")
val eventId =
uri.lastPathSegment?.toLongOrNull()
?: throw IllegalStateException("calendar insert failed")
return loadEventById(resolver, eventId)
?: throw IllegalStateException("calendar insert failed")
}
@@ -149,45 +166,54 @@ private object SystemCalendarDataSource : CalendarDataSource {
throw IllegalArgumentException("CALENDAR_NOT_FOUND: no default calendar")
}
private fun calendarExists(resolver: ContentResolver, id: Long): Boolean {
private fun calendarExists(
resolver: ContentResolver,
id: Long,
): Boolean {
val projection = arrayOf(CalendarContract.Calendars._ID)
resolver.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars._ID}=?",
arrayOf(id.toString()),
null,
).use { cursor ->
return cursor != null && cursor.moveToFirst()
}
resolver
.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars._ID}=?",
arrayOf(id.toString()),
null,
).use { cursor ->
return cursor != null && cursor.moveToFirst()
}
}
private fun findCalendarByTitle(resolver: ContentResolver, title: String): Long? {
private fun findCalendarByTitle(
resolver: ContentResolver,
title: String,
): Long? {
val projection = arrayOf(CalendarContract.Calendars._ID)
resolver.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}=?",
arrayOf(title),
"${CalendarContract.Calendars.IS_PRIMARY} DESC",
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getLong(0)
}
resolver
.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}=?",
arrayOf(title),
"${CalendarContract.Calendars.IS_PRIMARY} DESC",
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getLong(0)
}
}
private fun findDefaultCalendarId(resolver: ContentResolver): Long? {
val projection = arrayOf(CalendarContract.Calendars._ID)
resolver.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars.VISIBLE}=1",
null,
"${CalendarContract.Calendars.IS_PRIMARY} DESC, ${CalendarContract.Calendars._ID} ASC",
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getLong(0)
}
resolver
.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars.VISIBLE}=1",
null,
"${CalendarContract.Calendars.IS_PRIMARY} DESC, ${CalendarContract.Calendars._ID} ASC",
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getLong(0)
}
}
private fun loadEventById(
@@ -204,24 +230,30 @@ private object SystemCalendarDataSource : CalendarDataSource {
CalendarContract.Events.EVENT_LOCATION,
CalendarContract.Events.CALENDAR_DISPLAY_NAME,
)
resolver.query(
CalendarContract.Events.CONTENT_URI,
projection,
"${CalendarContract.Events._ID}=?",
arrayOf(eventId.toString()),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return CalendarEventRecord(
identifier = cursor.getLong(0).toString(),
title = cursor.getString(1)?.trim().orEmpty().ifEmpty { "(untitled)" },
startISO = Instant.ofEpochMilli(cursor.getLong(2)).toString(),
endISO = Instant.ofEpochMilli(cursor.getLong(3)).toString(),
isAllDay = cursor.getInt(4) == 1,
location = cursor.getString(5)?.trim()?.ifEmpty { null },
calendarTitle = cursor.getString(6)?.trim()?.ifEmpty { null },
)
}
resolver
.query(
CalendarContract.Events.CONTENT_URI,
projection,
"${CalendarContract.Events._ID}=?",
arrayOf(eventId.toString()),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return CalendarEventRecord(
identifier = cursor.getLong(0).toString(),
title =
cursor
.getString(1)
?.trim()
.orEmpty()
.ifEmpty { "(untitled)" },
startISO = Instant.ofEpochMilli(cursor.getLong(2)).toString(),
endISO = Instant.ofEpochMilli(cursor.getLong(3)).toString(),
isAllDay = cursor.getInt(4) == 1,
location = cursor.getString(5)?.trim()?.ifEmpty { null },
calendarTitle = cursor.getString(6)?.trim()?.ifEmpty { null },
)
}
}
}
@@ -337,10 +369,12 @@ class CalendarHandler private constructor(
} catch (_: Throwable) {
null
} ?: return null
val start = parseISO((params["startISO"] as? JsonPrimitive)?.content)
?: return null
val end = parseISO((params["endISO"] as? JsonPrimitive)?.content)
?: return null
val start =
parseISO((params["startISO"] as? JsonPrimitive)?.content)
?: return null
val end =
parseISO((params["endISO"] as? JsonPrimitive)?.content)
?: return null
return CalendarAddRequest(
title = (params["title"] as? JsonPrimitive)?.content?.trim().orEmpty(),
startMs = start.toEpochMilli(),
@@ -363,8 +397,8 @@ class CalendarHandler private constructor(
}
}
private fun eventJson(event: CalendarEventRecord): JsonObject {
return buildJsonObject {
private fun eventJson(event: CalendarEventRecord): JsonObject =
buildJsonObject {
put("identifier", JsonPrimitive(event.identifier))
put("title", JsonPrimitive(event.title))
put("startISO", JsonPrimitive(event.startISO))
@@ -373,7 +407,6 @@ class CalendarHandler private constructor(
event.location?.let { put("location", JsonPrimitive(it)) }
event.calendarTitle?.let { put("calendarTitle", JsonPrimitive(it)) }
}
}
companion object {
internal fun forTesting(

View File

@@ -1,16 +1,15 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.content.Context
import android.provider.CallLog
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
private const val DEFAULT_CALL_LOG_LIMIT = 25
@@ -38,26 +37,32 @@ internal data class CallLogSearchRequest(
internal interface CallLogDataSource {
fun hasReadPermission(context: Context): Boolean
fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord>
fun search(
context: Context,
request: CallLogSearchRequest,
): List<CallLogRecord>
}
private object SystemCallLogDataSource : CallLogDataSource {
override fun hasReadPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
override fun hasReadPermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CALL_LOG
Manifest.permission.READ_CALL_LOG,
) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
override fun search(
context: Context,
request: CallLogSearchRequest,
): List<CallLogRecord> {
val resolver = context.contentResolver
val projection = arrayOf(
CallLog.Calls.NUMBER,
CallLog.Calls.CACHED_NAME,
CallLog.Calls.DATE,
CallLog.Calls.DURATION,
CallLog.Calls.TYPE,
)
val projection =
arrayOf(
CallLog.Calls.NUMBER,
CallLog.Calls.CACHED_NAME,
CallLog.Calls.DATE,
CallLog.Calls.DURATION,
CallLog.Calls.TYPE,
)
// Build selection and selectionArgs for filtering
val selections = mutableListOf<String>()
@@ -105,40 +110,42 @@ private object SystemCallLogDataSource : CallLogDataSource {
val sortOrder = "${CallLog.Calls.DATE} DESC"
resolver.query(
CallLog.Calls.CONTENT_URI,
projection,
selection,
selectionArgsArray,
sortOrder,
).use { cursor ->
if (cursor == null) return emptyList()
resolver
.query(
CallLog.Calls.CONTENT_URI,
projection,
selection,
selectionArgsArray,
sortOrder,
).use { cursor ->
if (cursor == null) return emptyList()
val numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER)
val cachedNameIndex = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
val dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE)
val durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION)
val typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE)
val numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER)
val cachedNameIndex = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
val dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE)
val durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION)
val typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE)
// Skip offset rows
if (request.offset > 0 && cursor.moveToPosition(request.offset - 1)) {
// Successfully moved to offset position
// Skip offset rows
if (request.offset > 0 && cursor.moveToPosition(request.offset - 1)) {
// Successfully moved to offset position
}
val out = mutableListOf<CallLogRecord>()
var count = 0
while (cursor.moveToNext() && count < request.limit) {
out +=
CallLogRecord(
number = cursor.getString(numberIndex),
cachedName = cursor.getString(cachedNameIndex),
date = cursor.getLong(dateIndex),
duration = cursor.getLong(durationIndex),
type = cursor.getInt(typeIndex),
)
count++
}
return out
}
val out = mutableListOf<CallLogRecord>()
var count = 0
while (cursor.moveToNext() && count < request.limit) {
out += CallLogRecord(
number = cursor.getString(numberIndex),
cachedName = cursor.getString(cachedNameIndex),
date = cursor.getLong(dateIndex),
duration = cursor.getLong(durationIndex),
type = cursor.getInt(typeIndex),
)
count++
}
return out
}
}
}
@@ -156,11 +163,12 @@ class CallLogHandler private constructor(
)
}
val request = parseSearchRequest(paramsJson)
?: return GatewaySession.InvokeResult.error(
code = "INVALID_REQUEST",
message = "INVALID_REQUEST: expected JSON object",
)
val request =
parseSearchRequest(paramsJson)
?: return GatewaySession.InvokeResult.error(
code = "INVALID_REQUEST",
message = "INVALID_REQUEST: expected JSON object",
)
return try {
val callLogs = dataSource.search(appContext, request)
@@ -197,16 +205,19 @@ class CallLogHandler private constructor(
)
}
val params = try {
Json.parseToJsonElement(paramsJson).asObjectOrNull()
} catch (_: Throwable) {
null
} ?: return null
val params =
try {
Json.parseToJsonElement(paramsJson).asObjectOrNull()
} catch (_: Throwable) {
null
} ?: return null
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALL_LOG_LIMIT)
.coerceIn(1, 200)
val offset = ((params["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
.coerceAtLeast(0)
val limit =
((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALL_LOG_LIMIT)
.coerceIn(1, 200)
val offset =
((params["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
.coerceAtLeast(0)
val cachedName = (params["cachedName"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
val number = (params["number"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
val date = (params["date"] as? JsonPrimitive)?.content?.toLongOrNull()
@@ -228,15 +239,14 @@ class CallLogHandler private constructor(
)
}
private fun callLogJson(callLog: CallLogRecord): JsonObject {
return buildJsonObject {
private fun callLogJson(callLog: CallLogRecord): JsonObject =
buildJsonObject {
put("number", JsonPrimitive(callLog.number))
put("cachedName", JsonPrimitive(callLog.cachedName))
put("date", JsonPrimitive(callLog.date))
put("duration", JsonPrimitive(callLog.duration))
put("type", JsonPrimitive(callLog.type))
}
}
companion object {
internal fun forTesting(

View File

@@ -1,24 +1,23 @@
package ai.openclaw.app.node
import ai.openclaw.app.PermissionRequester
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.content.pm.PackageManager
import android.hardware.camera2.CameraCharacteristics
import android.util.Base64
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.CameraInfo
import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.LifecycleOwner
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
@@ -28,22 +27,33 @@ import androidx.camera.video.VideoRecordEvent
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.graphics.scale
import ai.openclaw.app.PermissionRequester
import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.concurrent.Executor
import kotlin.math.roundToInt
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.math.roundToInt
class CameraCaptureManager(
private val context: Context,
) {
data class Payload(
val payloadJson: String,
)
data class FilePayload(
val file: File,
val durationMs: Long,
val hasAudio: Boolean,
)
class CameraCaptureManager(private val context: Context) {
data class Payload(val payloadJson: String)
data class FilePayload(val file: File, val durationMs: Long, val hasAudio: Boolean)
data class CameraDeviceInfo(
val id: String,
val name: String,
@@ -52,6 +62,7 @@ class CameraCaptureManager(private val context: Context) {
)
@Volatile private var lifecycleOwner: LifecycleOwner? = null
@Volatile private var permissionRequester: PermissionRequester? = null
fun attachLifecycleOwner(owner: LifecycleOwner) {
@@ -74,8 +85,9 @@ class CameraCaptureManager(private val context: Context) {
val granted = checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
if (granted) return
val requester = permissionRequester
?: throw IllegalStateException("CAMERA_PERMISSION_REQUIRED: grant Camera permission")
val requester =
permissionRequester
?: throw IllegalStateException("CAMERA_PERMISSION_REQUIRED: grant Camera permission")
val results = requester.requestIfMissing(listOf(Manifest.permission.CAMERA))
if (results[Manifest.permission.CAMERA] != true) {
throw IllegalStateException("CAMERA_PERMISSION_REQUIRED: grant Camera permission")
@@ -86,8 +98,9 @@ class CameraCaptureManager(private val context: Context) {
val granted = checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
if (granted) return
val requester = permissionRequester
?: throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
val requester =
permissionRequester
?: throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
val results = requester.requestIfMissing(listOf(Manifest.permission.RECORD_AUDIO))
if (results[Manifest.permission.RECORD_AUDIO] != true) {
throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
@@ -111,9 +124,10 @@ class CameraCaptureManager(private val context: Context) {
provider.unbindAll()
provider.bindToLifecycle(owner, selector, capture)
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor())
val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor(), context.cacheDir)
val decoded =
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
val rotated = rotateBitmapByExif(decoded, orientation)
val scaled =
if (maxWidth > 0 && rotated.width > maxWidth) {
@@ -177,23 +191,30 @@ class CameraCaptureManager(private val context: Context) {
val deviceId = parseDeviceId(params)
if (includeAudio) ensureMicPermission()
android.util.Log.w("CameraCaptureManager", "clip: start facing=$facing duration=$durationMs audio=$includeAudio deviceId=${deviceId ?: "-"}")
android.util.Log.w(
"CameraCaptureManager",
"clip: start facing=$facing duration=$durationMs audio=$includeAudio deviceId=${deviceId ?: "-"}",
)
val provider = context.cameraProvider()
android.util.Log.w("CameraCaptureManager", "clip: got camera provider")
// Use LOWEST quality for smallest files over WebSocket
val recorder = Recorder.Builder()
.setQualitySelector(
QualitySelector.from(Quality.LOWEST, FallbackStrategy.lowerQualityOrHigherThan(Quality.LOWEST))
)
.build()
val recorder =
Recorder
.Builder()
.setQualitySelector(
QualitySelector.from(Quality.LOWEST, FallbackStrategy.lowerQualityOrHigherThan(Quality.LOWEST)),
).build()
val videoCapture = VideoCapture.withOutput(recorder)
val selector = resolveCameraSelector(provider, facing, deviceId)
// CameraX requires a Preview use case for the camera to start producing frames;
// without it, the encoder may get no data (ERROR_NO_VALID_DATA).
val preview = androidx.camera.core.Preview.Builder().build()
val preview =
androidx.camera.core.Preview
.Builder()
.build()
// Provide a dummy SurfaceTexture so the preview pipeline activates
val surfaceTexture = android.graphics.SurfaceTexture(0)
surfaceTexture.setDefaultBufferSize(640, 480)
@@ -214,7 +235,7 @@ class CameraCaptureManager(private val context: Context) {
android.util.Log.w("CameraCaptureManager", "clip: warming up camera 1.5s...")
kotlinx.coroutines.delay(1_500)
val file = File.createTempFile("openclaw-clip-", ".mp4")
val file = File.createTempFile("openclaw-clip-", ".mp4", context.cacheDir)
val outputOptions = FileOutputOptions.Builder(file).build()
val finalized = kotlinx.coroutines.CompletableDeferred<VideoRecordEvent.Finalize>()
@@ -224,14 +245,16 @@ class CameraCaptureManager(private val context: Context) {
.prepareRecording(context, outputOptions)
.apply {
if (includeAudio) withAudioEnabled()
}
.start(context.mainExecutor()) { event ->
}.start(context.mainExecutor()) { event ->
android.util.Log.w("CameraCaptureManager", "clip: event ${event.javaClass.simpleName}")
if (event is VideoRecordEvent.Status) {
android.util.Log.w("CameraCaptureManager", "clip: recording status update")
}
if (event is VideoRecordEvent.Finalize) {
android.util.Log.w("CameraCaptureManager", "clip: finalize hasError=${event.hasError()} error=${event.error} cause=${event.cause}")
android.util.Log.w(
"CameraCaptureManager",
"clip: finalize hasError=${event.hasError()} error=${event.error} cause=${event.cause}",
)
finalized.complete(event)
}
}
@@ -254,7 +277,11 @@ class CameraCaptureManager(private val context: Context) {
throw IllegalStateException("UNAVAILABLE: camera clip finalize timed out")
}
if (finalizeEvent.hasError()) {
android.util.Log.e("CameraCaptureManager", "clip: FAILED error=${finalizeEvent.error}, cause=${finalizeEvent.cause}", finalizeEvent.cause)
android.util.Log.e(
"CameraCaptureManager",
"clip: FAILED error=${finalizeEvent.error}, cause=${finalizeEvent.cause}",
finalizeEvent.cause,
)
// Check file size for debugging
val fileSize = withContext(Dispatchers.IO) { if (file.exists()) file.length() else -1 }
android.util.Log.e("CameraCaptureManager", "clip: file exists=${file.exists()} size=$fileSize")
@@ -271,7 +298,10 @@ class CameraCaptureManager(private val context: Context) {
FilePayload(file = file, durationMs = durationMs.toLong(), hasAudio = includeAudio)
}
private fun rotateBitmapByExif(bitmap: Bitmap, orientation: Int): Bitmap {
private fun rotateBitmapByExif(
bitmap: Bitmap,
orientation: Int,
): Bitmap {
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
@@ -304,15 +334,13 @@ class CameraCaptureManager(private val context: Context) {
}
}
private fun parseQuality(params: JsonObject?): Double? =
parseJsonDouble(params, "quality")
private fun parseQuality(params: JsonObject?): Double? = parseJsonDouble(params, "quality")
private fun parseMaxWidth(params: JsonObject?): Int? =
parseJsonInt(params, "maxWidth")
?.takeIf { it > 0 }
private fun parseDurationMs(params: JsonObject?): Int? =
parseJsonInt(params, "durationMs")
private fun parseDurationMs(params: JsonObject?): Int? = parseJsonInt(params, "durationMs")
private fun parseDeviceId(params: JsonObject?): String? =
parseJsonString(params, "deviceId")
@@ -335,7 +363,8 @@ class CameraCaptureManager(private val context: Context) {
if (!availableIds.contains(deviceId)) {
throw IllegalStateException("INVALID_REQUEST: unknown camera deviceId '$deviceId'")
}
return CameraSelector.Builder()
return CameraSelector
.Builder()
.addCameraFilter { infos -> infos.filter { cameraIdOrNull(it) == deviceId } }
.build()
}
@@ -372,8 +401,7 @@ class CameraCaptureManager(private val context: Context) {
}
@SuppressLint("UnsafeOptInUsageError")
private fun cameraIdOrNull(info: CameraInfo): String? =
runCatching { Camera2CameraInfo.from(info).cameraId }.getOrNull()
private fun cameraIdOrNull(info: CameraInfo): String? = runCatching { Camera2CameraInfo.from(info).cameraId }.getOrNull()
}
private suspend fun Context.cameraProvider(): ProcessCameraProvider =
@@ -392,9 +420,12 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider =
}
/** Returns (jpegBytes, exifOrientation) so caller can rotate the decoded bitmap. */
private suspend fun ImageCapture.takeJpegWithExif(executor: Executor): Pair<ByteArray, Int> =
private suspend fun ImageCapture.takeJpegWithExif(
executor: Executor,
tempDir: File,
): Pair<ByteArray, Int> =
suspendCancellableCoroutine { cont ->
val file = File.createTempFile("openclaw-snap-", ".jpg")
val file = File.createTempFile("openclaw-snap-", ".jpg", tempDir)
val options = ImageCapture.OutputFileOptions.Builder(file).build()
takePicture(
options,
@@ -408,10 +439,11 @@ private suspend fun ImageCapture.takeJpegWithExif(executor: Executor): Pair<Byte
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
try {
val exif = ExifInterface(file.absolutePath)
val orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL,
)
val orientation =
exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL,
)
val bytes = file.readBytes()
cont.resume(Pair(bytes, orientation))
} catch (e: Exception) {

View File

@@ -1,9 +1,9 @@
package ai.openclaw.app.node
import android.content.Context
import ai.openclaw.app.CameraHudKind
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.CameraHudKind
import ai.openclaw.app.gateway.GatewaySession
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
@@ -16,8 +16,7 @@ import kotlinx.serialization.json.put
internal const val CAMERA_CLIP_MAX_RAW_BYTES: Long = 18L * 1024L * 1024L
internal fun isCameraClipWithinPayloadLimit(rawBytes: Long): Boolean =
rawBytes in 0L..CAMERA_CLIP_MAX_RAW_BYTES
internal fun isCameraClipWithinPayloadLimit(rawBytes: Long): Boolean = rawBytes in 0L..CAMERA_CLIP_MAX_RAW_BYTES
class CameraHandler(
private val appContext: Context,
@@ -27,8 +26,8 @@ class CameraHandler(
private val triggerCameraFlash: () -> Unit,
private val invokeErrorFromThrowable: (err: Throwable) -> Pair<String, String>,
) {
suspend fun handleList(_paramsJson: String?): GatewaySession.InvokeResult {
return try {
suspend fun handleList(_paramsJson: String?): GatewaySession.InvokeResult =
try {
val devices = camera.listDevices()
val payload =
buildJsonObject {
@@ -53,10 +52,10 @@ class CameraHandler(
val (code, message) = invokeErrorFromThrowable(err)
GatewaySession.InvokeResult.error(code = code, message = message)
}
}
suspend fun handleSnap(paramsJson: String?): GatewaySession.InvokeResult {
val logFile = if (BuildConfig.DEBUG) java.io.File(appContext.cacheDir, "camera_debug.log") else null
fun camLog(msg: String) {
if (!BuildConfig.DEBUG) return
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(java.util.Date())
@@ -95,6 +94,7 @@ class CameraHandler(
suspend fun handleClip(paramsJson: String?): GatewaySession.InvokeResult {
val clipLogFile = if (BuildConfig.DEBUG) java.io.File(appContext.cacheDir, "camera_debug.log") else null
fun clipLog(msg: String) {
if (!BuildConfig.DEBUG) return
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(java.util.Date())
@@ -133,18 +133,19 @@ class CameraHandler(
)
}
val bytes = withContext(Dispatchers.IO) {
try {
filePayload.file.readBytes()
} finally {
filePayload.file.delete()
val bytes =
withContext(Dispatchers.IO) {
try {
filePayload.file.readBytes()
} finally {
filePayload.file.delete()
}
}
}
val base64 = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
clipLog("returning base64 payload")
showCameraHud("Clip captured", CameraHudKind.Success, 1800)
return GatewaySession.InvokeResult.ok(
"""{"format":"mp4","base64":"$base64","durationMs":${filePayload.durationMs},"hasAudio":${filePayload.hasAudio}}"""
"""{"format":"mp4","base64":"$base64","durationMs":${filePayload.durationMs},"hasAudio":${filePayload.hasAudio}}""",
)
} catch (err: Throwable) {
clipLog("outer error: ${err::class.java.simpleName}: ${err.message}")

View File

@@ -5,7 +5,10 @@ import java.net.URI
object CanvasActionTrust {
const val scaffoldAssetUrl: String = "file:///android_asset/CanvasScaffold/scaffold.html"
fun isTrustedCanvasActionUrl(rawUrl: String?, trustedA2uiUrls: List<String>): Boolean {
fun isTrustedCanvasActionUrl(
rawUrl: String?,
trustedA2uiUrls: List<String>,
): Boolean {
val candidate = rawUrl?.trim().orEmpty()
if (candidate.isEmpty()) return false
if (candidate == scaffoldAssetUrl) return true
@@ -21,7 +24,10 @@ object CanvasActionTrust {
}
}
private fun matchesTrustedRemoteA2uiUrlExact(candidateUri: URI, trustedUrl: String): Boolean {
private fun matchesTrustedRemoteA2uiUrlExact(
candidateUri: URI,
trustedUrl: String,
): Boolean {
val trustedUri = parseUri(trustedUrl) ?: return false
val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false
return candidateUri == normalizedTrusted
@@ -33,7 +39,11 @@ object CanvasActionTrust {
val scheme = uri.scheme?.lowercase() ?: return null
if (scheme != "http" && scheme != "https") return null
val host = uri.host?.trim()?.takeIf { it.isNotEmpty() }?.lowercase() ?: return null
val host =
uri.host
?.trim()
?.takeIf { it.isNotEmpty() }
?.lowercase() ?: return null
return try {
URI(scheme, uri.userInfo, host, uri.port, uri.rawPath, uri.rawQuery, null)

View File

@@ -1,39 +1,46 @@
package ai.openclaw.app.node
import ai.openclaw.app.BuildConfig
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Looper
import android.util.Base64
import android.util.Log
import android.webkit.WebView
import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.io.ByteArrayOutputStream
import android.util.Base64
import org.json.JSONObject
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import ai.openclaw.app.BuildConfig
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import kotlin.coroutines.resume
class CanvasController {
enum class SnapshotFormat(val rawValue: String) {
enum class SnapshotFormat(
val rawValue: String,
) {
Png("png"),
Jpeg("jpeg"),
}
@Volatile private var webView: WebView? = null
@Volatile private var url: String? = null
@Volatile private var debugStatusEnabled: Boolean = false
@Volatile private var debugStatusTitle: String? = null
@Volatile private var debugStatusSubtitle: String? = null
@Volatile private var homeCanvasStateJson: String? = null
private val _currentUrl = MutableStateFlow<String?>(null)
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
@@ -82,7 +89,10 @@ class CanvasController {
applyDebugStatus()
}
fun setDebugStatus(title: String?, subtitle: String?) {
fun setDebugStatus(
title: String?,
subtitle: String?,
) {
debugStatusTitle = title
debugStatusSubtitle = subtitle
applyDebugStatus()
@@ -131,7 +141,8 @@ class CanvasController {
withWebViewOnMain { wv ->
val titleJs = title?.let { JSONObject.quote(it) } ?: "null"
val subtitleJs = subtitle?.let { JSONObject.quote(it) } ?: "null"
val js = """
val js =
"""
(() => {
try {
const api = globalThis.__openclaw;
@@ -145,7 +156,7 @@ class CanvasController {
}
} catch (_) {}
})();
""".trimIndent()
""".trimIndent()
wv.evaluateJavascript(js, null)
}
}
@@ -153,7 +164,8 @@ class CanvasController {
private fun applyHomeCanvasState() {
val payload = homeCanvasStateJson ?: "null"
withWebViewOnMain { wv ->
val js = """
val js =
"""
(() => {
try {
const api = globalThis.__openclaw;
@@ -161,7 +173,7 @@ class CanvasController {
api.renderHome($payload);
} catch (_) {}
})();
""".trimIndent()
""".trimIndent()
wv.evaluateJavascript(js, null)
}
}
@@ -194,7 +206,11 @@ class CanvasController {
}
}
suspend fun snapshotBase64(format: SnapshotFormat, quality: Double?, maxWidth: Int?): String =
suspend fun snapshotBase64(
format: SnapshotFormat,
quality: Double?,
maxWidth: Int?,
): String =
withContext(Dispatchers.Main) {
val wv = webView ?: throw IllegalStateException("no webview")
val bmp = wv.captureBitmap()
@@ -230,7 +246,11 @@ class CanvasController {
}
companion object {
data class SnapshotParams(val format: SnapshotFormat, val quality: Double?, val maxWidth: Int?)
data class SnapshotParams(
val format: SnapshotFormat,
val quality: Double?,
val maxWidth: Int?,
)
fun parseNavigateUrl(paramsJson: String?): String {
val obj = parseParamsObject(paramsJson) ?: return ""
@@ -269,13 +289,12 @@ class CanvasController {
return q.coerceIn(0.1, 1.0)
}
fun parseSnapshotParams(paramsJson: String?): SnapshotParams {
return SnapshotParams(
fun parseSnapshotParams(paramsJson: String?): SnapshotParams =
SnapshotParams(
format = parseSnapshotFormat(paramsJson),
quality = parseSnapshotQuality(paramsJson),
maxWidth = parseSnapshotMaxWidth(paramsJson),
)
}
private val json = Json { ignoreUnknownKeys = true }

View File

@@ -1,15 +1,15 @@
package ai.openclaw.app.node
import android.os.Build
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.LocationMode
import ai.openclaw.app.SecurePrefs
import ai.openclaw.app.VoiceWakeMode
import ai.openclaw.app.gateway.GatewayClientInfo
import ai.openclaw.app.gateway.GatewayConnectOptions
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayTlsParams
import ai.openclaw.app.gateway.isLoopbackGatewayHost
import ai.openclaw.app.LocationMode
import ai.openclaw.app.VoiceWakeMode
import android.os.Build
class ConnectionManager(
private val prefs: SecurePrefs,
@@ -115,22 +115,27 @@ class ConnectionManager(
}
}
fun resolveModelIdentifier(): String? {
return listOfNotNull(Build.MANUFACTURER, Build.MODEL)
fun resolveModelIdentifier(): String? =
listOfNotNull(Build.MANUFACTURER, Build.MODEL)
.joinToString(" ")
.trim()
.ifEmpty { null }
}
fun buildUserAgent(): String {
val version = resolvedVersionName()
val release = Build.VERSION.RELEASE?.trim().orEmpty()
val release =
Build.VERSION.RELEASE
?.trim()
.orEmpty()
val releaseLabel = if (release.isEmpty()) "unknown" else release
return "OpenClawAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})"
}
fun buildClientInfo(clientId: String, clientMode: String): GatewayClientInfo {
return GatewayClientInfo(
fun buildClientInfo(
clientId: String,
clientMode: String,
): GatewayClientInfo =
GatewayClientInfo(
id = clientId,
displayName = prefs.displayName.value,
version = resolvedVersionName(),
@@ -140,10 +145,9 @@ class ConnectionManager(
deviceFamily = "Android",
modelIdentifier = resolveModelIdentifier(),
)
}
fun buildNodeConnectOptions(): GatewayConnectOptions {
return GatewayConnectOptions(
fun buildNodeConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "node",
scopes = emptyList(),
caps = buildCapabilities(),
@@ -152,10 +156,9 @@ class ConnectionManager(
client = buildClientInfo(clientId = "openclaw-android", clientMode = "node"),
userAgent = buildUserAgent(),
)
}
fun buildOperatorConnectOptions(): GatewayConnectOptions {
return GatewayConnectOptions(
fun buildOperatorConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "operator",
scopes = listOf("operator.read", "operator.write", "operator.talk.secrets"),
caps = emptyList(),
@@ -164,7 +167,6 @@ class ConnectionManager(
client = buildClientInfo(clientId = "openclaw-android", clientMode = "ui"),
userAgent = buildUserAgent(),
)
}
fun resolveTlsParams(endpoint: GatewayEndpoint): GatewayTlsParams? {
val stored = prefs.loadGatewayTlsFingerprint(endpoint.stableId)

View File

@@ -1,13 +1,12 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.content.ContentProviderOperation
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -47,23 +46,30 @@ internal interface ContactsDataSource {
fun hasWritePermission(context: Context): Boolean
fun search(context: Context, request: ContactsSearchRequest): List<ContactRecord>
fun search(
context: Context,
request: ContactsSearchRequest,
): List<ContactRecord>
fun add(context: Context, request: ContactsAddRequest): ContactRecord
fun add(
context: Context,
request: ContactsAddRequest,
): ContactRecord
}
private object SystemContactsDataSource : ContactsDataSource {
override fun hasReadPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) ==
override fun hasReadPermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun hasWritePermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) ==
override fun hasWritePermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun search(context: Context, request: ContactsSearchRequest): List<ContactRecord> {
override fun search(
context: Context,
request: ContactsSearchRequest,
): List<ContactRecord> {
val resolver = context.contentResolver
val projection =
arrayOf(
@@ -80,37 +86,43 @@ private object SystemContactsDataSource : ContactsDataSource {
selectionArgs = arrayOf("%${escapeLikePattern(request.query)}%")
}
val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} COLLATE NOCASE ASC LIMIT ${request.limit}"
resolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder,
).use { cursor ->
if (cursor == null) return emptyList()
val idIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
val displayNameIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
val out = mutableListOf<ContactRecord>()
while (cursor.moveToNext() && out.size < request.limit) {
val contactId = cursor.getLong(idIndex)
val displayName = cursor.getString(displayNameIndex).orEmpty()
out += loadContactRecord(resolver, contactId, fallbackDisplayName = displayName)
resolver
.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder,
).use { cursor ->
if (cursor == null) return emptyList()
val idIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
val displayNameIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
val out = mutableListOf<ContactRecord>()
while (cursor.moveToNext() && out.size < request.limit) {
val contactId = cursor.getLong(idIndex)
val displayName = cursor.getString(displayNameIndex).orEmpty()
out += loadContactRecord(resolver, contactId, fallbackDisplayName = displayName)
}
return out
}
return out
}
}
override fun add(context: Context, request: ContactsAddRequest): ContactRecord {
override fun add(
context: Context,
request: ContactsAddRequest,
): ContactRecord {
val resolver = context.contentResolver
val operations = ArrayList<ContentProviderOperation>()
operations +=
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
ContentProviderOperation
.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build()
if (!request.givenName.isNullOrEmpty() || !request.familyName.isNullOrEmpty() || !request.displayName.isNullOrEmpty()) {
operations +=
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, request.givenName)
@@ -120,7 +132,8 @@ private object SystemContactsDataSource : ContactsDataSource {
}
if (!request.organizationName.isNullOrEmpty()) {
operations +=
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, request.organizationName)
@@ -128,7 +141,8 @@ private object SystemContactsDataSource : ContactsDataSource {
}
request.phoneNumbers.forEach { number ->
operations +=
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, number)
@@ -137,7 +151,8 @@ private object SystemContactsDataSource : ContactsDataSource {
}
request.emails.forEach { email ->
operations +=
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
@@ -146,12 +161,15 @@ private object SystemContactsDataSource : ContactsDataSource {
}
val results = resolver.applyBatch(ContactsContract.AUTHORITY, operations)
val rawContactUri = results.firstOrNull()?.uri
?: throw IllegalStateException("contact insert failed")
val rawContactId = rawContactUri.lastPathSegment?.toLongOrNull()
?: throw IllegalStateException("contact insert failed")
val contactId = resolveContactIdForRawContact(resolver, rawContactId)
?: throw IllegalStateException("contact insert failed")
val rawContactUri =
results.firstOrNull()?.uri
?: throw IllegalStateException("contact insert failed")
val rawContactId =
rawContactUri.lastPathSegment?.toLongOrNull()
?: throw IllegalStateException("contact insert failed")
val contactId =
resolveContactIdForRawContact(resolver, rawContactId)
?: throw IllegalStateException("contact insert failed")
return loadContactRecord(
resolver = resolver,
contactId = contactId,
@@ -159,19 +177,23 @@ private object SystemContactsDataSource : ContactsDataSource {
)
}
private fun resolveContactIdForRawContact(resolver: ContentResolver, rawContactId: Long): Long? {
private fun resolveContactIdForRawContact(
resolver: ContentResolver,
rawContactId: Long,
): Long? {
val projection = arrayOf(ContactsContract.RawContacts.CONTACT_ID)
resolver.query(
ContactsContract.RawContacts.CONTENT_URI,
projection,
"${ContactsContract.RawContacts._ID}=?",
arrayOf(rawContactId.toString()),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
val index = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID)
return cursor.getLong(index)
}
resolver
.query(
ContactsContract.RawContacts.CONTENT_URI,
projection,
"${ContactsContract.RawContacts._ID}=?",
arrayOf(rawContactId.toString()),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
val index = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID)
return cursor.getLong(index)
}
}
private fun loadContactRecord(
@@ -206,69 +228,80 @@ private object SystemContactsDataSource : ContactsDataSource {
val displayName: String?,
)
private fun loadNameRow(resolver: ContentResolver, contactId: Long): NameRow {
private fun loadNameRow(
resolver: ContentResolver,
contactId: Long,
): NameRow {
val projection =
arrayOf(
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
)
resolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
"${ContactsContract.Data.CONTACT_ID}=? AND ${ContactsContract.Data.MIMETYPE}=?",
arrayOf(
contactId.toString(),
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) {
return NameRow(givenName = null, familyName = null, displayName = null)
resolver
.query(
ContactsContract.Data.CONTENT_URI,
projection,
"${ContactsContract.Data.CONTACT_ID}=? AND ${ContactsContract.Data.MIMETYPE}=?",
arrayOf(
contactId.toString(),
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) {
return NameRow(givenName = null, familyName = null, displayName = null)
}
val given = cursor.getString(0)?.trim()?.ifEmpty { null }
val family = cursor.getString(1)?.trim()?.ifEmpty { null }
val display = cursor.getString(2)?.trim()?.ifEmpty { null }
return NameRow(givenName = given, familyName = family, displayName = display)
}
val given = cursor.getString(0)?.trim()?.ifEmpty { null }
val family = cursor.getString(1)?.trim()?.ifEmpty { null }
val display = cursor.getString(2)?.trim()?.ifEmpty { null }
return NameRow(givenName = given, familyName = family, displayName = display)
}
}
private fun loadOrganization(resolver: ContentResolver, contactId: Long): String? {
private fun loadOrganization(
resolver: ContentResolver,
contactId: Long,
): String? {
val projection = arrayOf(ContactsContract.CommonDataKinds.Organization.COMPANY)
resolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
"${ContactsContract.Data.CONTACT_ID}=? AND ${ContactsContract.Data.MIMETYPE}=?",
arrayOf(contactId.toString(), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getString(0)?.trim()?.ifEmpty { null }
}
resolver
.query(
ContactsContract.Data.CONTENT_URI,
projection,
"${ContactsContract.Data.CONTACT_ID}=? AND ${ContactsContract.Data.MIMETYPE}=?",
arrayOf(contactId.toString(), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE),
null,
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
return cursor.getString(0)?.trim()?.ifEmpty { null }
}
}
private fun escapeLikePattern(pattern: String): String =
pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
private fun escapeLikePattern(pattern: String): String = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
private fun loadPhones(resolver: ContentResolver, contactId: Long): List<String> {
return queryContactValues(
private fun loadPhones(
resolver: ContentResolver,
contactId: Long,
): List<String> =
queryContactValues(
resolver = resolver,
contentUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
valueColumn = ContactsContract.CommonDataKinds.Phone.NUMBER,
contactIdColumn = ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
contactId = contactId,
)
}
private fun loadEmails(resolver: ContentResolver, contactId: Long): List<String> {
return queryContactValues(
private fun loadEmails(
resolver: ContentResolver,
contactId: Long,
): List<String> =
queryContactValues(
resolver = resolver,
contentUri = ContactsContract.CommonDataKinds.Email.CONTENT_URI,
valueColumn = ContactsContract.CommonDataKinds.Email.ADDRESS,
contactIdColumn = ContactsContract.CommonDataKinds.Email.CONTACT_ID,
contactId = contactId,
)
}
private fun queryContactValues(
resolver: ContentResolver,
@@ -278,21 +311,22 @@ private object SystemContactsDataSource : ContactsDataSource {
contactId: Long,
): List<String> {
val projection = arrayOf(valueColumn)
resolver.query(
contentUri,
projection,
"$contactIdColumn=?",
arrayOf(contactId.toString()),
null,
).use { cursor ->
if (cursor == null) return emptyList()
val out = LinkedHashSet<String>()
while (cursor.moveToNext()) {
val value = cursor.getString(0)?.trim().orEmpty()
if (value.isNotEmpty()) out += value
resolver
.query(
contentUri,
projection,
"$contactIdColumn=?",
arrayOf(contactId.toString()),
null,
).use { cursor ->
if (cursor == null) return emptyList()
val out = LinkedHashSet<String>()
while (cursor.moveToNext()) {
val value = cursor.getString(0)?.trim().orEmpty()
if (value.isNotEmpty()) out += value
}
return out.toList()
}
return out.toList()
}
}
}
@@ -412,8 +446,8 @@ class ContactsHandler private constructor(
}
}
private fun contactJson(contact: ContactRecord): JsonObject {
return buildJsonObject {
private fun contactJson(contact: ContactRecord): JsonObject =
buildJsonObject {
put("identifier", JsonPrimitive(contact.identifier))
put("displayName", JsonPrimitive(contact.displayName))
put("givenName", JsonPrimitive(contact.givenName))
@@ -422,7 +456,6 @@ class ContactsHandler private constructor(
put("phoneNumbers", buildJsonArray { contact.phoneNumbers.forEach { add(JsonPrimitive(it)) } })
put("emails", buildJsonArray { contact.emails.forEach { add(JsonPrimitive(it)) } })
}
}
companion object {
internal fun forTesting(

View File

@@ -1,16 +1,17 @@
package ai.openclaw.app.node
import android.content.Context
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import android.content.Context
import kotlinx.serialization.json.JsonPrimitive
private const val LOGCAT_PATH = "/system/bin/logcat"
class DebugHandler(
private val appContext: Context,
private val identityStore: DeviceIdentityStore,
) {
fun handleEd25519(): GatewaySession.InvokeResult {
if (!BuildConfig.DEBUG) {
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = "debug commands are disabled in release builds")
@@ -40,9 +41,10 @@ class DebugHandler(
// Check available providers
val providers = java.security.Security.getProviders()
val ed25519Providers = providers.filter { p ->
p.services.any { s -> s.algorithm.contains("Ed25519", ignoreCase = true) }
}
val ed25519Providers =
providers.filter { p ->
p.services.any { s -> s.algorithm.contains("Ed25519", ignoreCase = true) }
}
results.add("Ed25519 providers: ${ed25519Providers.map { "${it.name} v${it.version}" }}")
results.add("Provider order: ${providers.take(5).map { it.name }}")
@@ -65,7 +67,10 @@ class DebugHandler(
val diagnostics = results.joinToString("\n")
return GatewaySession.InvokeResult.ok("""{"diagnostics":${JsonPrimitive(diagnostics)}}""")
} catch (e: Throwable) {
return GatewaySession.InvokeResult.error(code = "ED25519_TEST_FAILED", message = "${e.javaClass.simpleName}: ${e.message}\n${e.stackTraceToString().take(500)}")
return GatewaySession.InvokeResult.error(
code = "ED25519_TEST_FAILED",
message = "${e.javaClass.simpleName}: ${e.message}\n${e.stackTraceToString().take(500)}",
)
}
}
@@ -75,44 +80,67 @@ class DebugHandler(
}
val pid = android.os.Process.myPid()
val rt = Runtime.getRuntime()
val info = "v6 pid=$pid thread=${Thread.currentThread().name} free=${rt.freeMemory()/1024}K total=${rt.totalMemory()/1024}K max=${rt.maxMemory()/1024}K uptime=${android.os.SystemClock.elapsedRealtime()/1000}s sdk=${android.os.Build.VERSION.SDK_INT} device=${android.os.Build.MODEL}\n"
val info = "v6 pid=$pid thread=${Thread.currentThread().name} free=${rt.freeMemory() / 1024}K total=${rt.totalMemory() / 1024}K max=${rt.maxMemory() / 1024}K uptime=${android.os.SystemClock.elapsedRealtime() / 1000}s sdk=${android.os.Build.VERSION.SDK_INT} device=${android.os.Build.MODEL}\n"
// Run logcat on current dispatcher thread (no withContext) with file redirect
val logResult = try {
val tmpFile = java.io.File(appContext.cacheDir, "debug_logs.txt")
if (tmpFile.exists()) tmpFile.delete()
val pb = ProcessBuilder("logcat", "-d", "-t", "200", "--pid=$pid")
pb.redirectOutput(tmpFile)
pb.redirectErrorStream(true)
val proc = pb.start()
val finished = proc.waitFor(4, java.util.concurrent.TimeUnit.SECONDS)
if (!finished) proc.destroyForcibly()
val raw = if (tmpFile.exists() && tmpFile.length() > 0) {
tmpFile.readText().take(128000)
} else {
"(no output, finished=$finished, exists=${tmpFile.exists()})"
val logResult =
try {
val tmpFile = java.io.File(appContext.cacheDir, "debug_logs.txt")
if (tmpFile.exists()) tmpFile.delete()
val pb = ProcessBuilder(LOGCAT_PATH, "-d", "-t", "200", "--pid=$pid")
pb.redirectOutput(tmpFile)
pb.redirectErrorStream(true)
val proc = pb.start()
val finished = proc.waitFor(4, java.util.concurrent.TimeUnit.SECONDS)
if (!finished) proc.destroyForcibly()
val raw =
if (tmpFile.exists() && tmpFile.length() > 0) {
tmpFile.readText().take(128000)
} else {
"(no output, finished=$finished, exists=${tmpFile.exists()})"
}
tmpFile.delete()
val spamPatterns =
listOf(
"setRequestedFrameRate",
"I View :",
"BLASTBufferQueue",
"VRI[Pop-Up",
"InsetsController:",
"VRI[MainActivity",
"InsetsSource:",
"handleResized",
"ProfileInstaller",
"I VRI[",
"onStateChanged: host=",
"D StrictMode:",
"E StrictMode:",
"ImeFocusController",
"InputTransport",
"IncorrectContextUseViolation",
)
val sb = StringBuilder()
for (line in raw.lineSequence()) {
if (line.isBlank()) continue
if (spamPatterns.any { line.contains(it) }) continue
if (sb.length + line.length > 16000) {
sb.append("\n(truncated)")
break
}
if (sb.isNotEmpty()) sb.append('\n')
sb.append(line)
}
sb.toString().ifEmpty { "(all ${raw.lines().size} lines filtered as spam)" }
} catch (e: Throwable) {
"(logcat error: ${e::class.java.simpleName}: ${e.message})"
}
tmpFile.delete()
val spamPatterns = listOf("setRequestedFrameRate", "I View :", "BLASTBufferQueue", "VRI[Pop-Up",
"InsetsController:", "VRI[MainActivity", "InsetsSource:", "handleResized", "ProfileInstaller",
"I VRI[", "onStateChanged: host=", "D StrictMode:", "E StrictMode:", "ImeFocusController",
"InputTransport", "IncorrectContextUseViolation")
val sb = StringBuilder()
for (line in raw.lineSequence()) {
if (line.isBlank()) continue
if (spamPatterns.any { line.contains(it) }) continue
if (sb.length + line.length > 16000) { sb.append("\n(truncated)"); break }
if (sb.isNotEmpty()) sb.append('\n')
sb.append(line)
}
sb.toString().ifEmpty { "(all ${raw.lines().size} lines filtered as spam)" }
} catch (e: Throwable) {
"(logcat error: ${e::class.java.simpleName}: ${e.message})"
}
// Also include camera debug log if it exists
val camLogFile = java.io.File(appContext.cacheDir, "camera_debug.log")
val camLog = if (camLogFile.exists() && camLogFile.length() > 0) {
"\n--- camera_debug.log ---\n" + camLogFile.readText().take(4000)
} else ""
val camLog =
if (camLogFile.exists() && camLogFile.length() > 0) {
"\n--- camera_debug.log ---\n" + camLogFile.readText().take(4000)
} else {
""
}
return GatewaySession.InvokeResult.ok("""{"logs":${JsonPrimitive(info + logResult + camLog)}}""")
}
}

View File

@@ -1,6 +1,7 @@
package ai.openclaw.app.node
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.app.ActivityManager
import android.content.Context
@@ -16,13 +17,11 @@ import android.os.PowerManager
import android.os.StatFs
import android.os.SystemClock
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.util.Locale
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.util.Locale
class DeviceHandler(
private val appContext: Context,
@@ -35,19 +34,16 @@ class DeviceHandler(
telephonyAvailable: Boolean,
smsSendGranted: Boolean,
smsReadGranted: Boolean,
): Boolean {
return smsEnabled && telephonyAvailable && (smsSendGranted || smsReadGranted)
}
): Boolean = smsEnabled && telephonyAvailable && (smsSendGranted || smsReadGranted)
internal fun isSmsPromptable(
smsEnabled: Boolean,
telephonyAvailable: Boolean,
smsSendGranted: Boolean,
smsReadGranted: Boolean,
): Boolean {
return smsEnabled && telephonyAvailable && (!smsSendGranted || !smsReadGranted)
}
): Boolean = smsEnabled && telephonyAvailable && (!smsSendGranted || !smsReadGranted)
}
private data class BatterySnapshot(
val status: Int,
val plugged: Int,
@@ -55,21 +51,13 @@ class DeviceHandler(
val temperatureC: Double?,
)
fun handleDeviceStatus(_paramsJson: String?): GatewaySession.InvokeResult {
return GatewaySession.InvokeResult.ok(statusPayloadJson())
}
fun handleDeviceStatus(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(statusPayloadJson())
fun handleDeviceInfo(_paramsJson: String?): GatewaySession.InvokeResult {
return GatewaySession.InvokeResult.ok(infoPayloadJson())
}
fun handleDeviceInfo(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(infoPayloadJson())
fun handleDevicePermissions(_paramsJson: String?): GatewaySession.InvokeResult {
return GatewaySession.InvokeResult.ok(permissionsPayloadJson())
}
fun handleDevicePermissions(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(permissionsPayloadJson())
fun handleDeviceHealth(_paramsJson: String?): GatewaySession.InvokeResult {
return GatewaySession.InvokeResult.ok(healthPayloadJson())
}
fun handleDeviceHealth(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(healthPayloadJson())
private fun statusPayloadJson(): String {
val battery = readBatterySnapshot()
@@ -133,14 +121,20 @@ class DeviceHandler(
val model = Build.MODEL?.trim().orEmpty()
val manufacturer = Build.MANUFACTURER?.trim().orEmpty()
val modelIdentifier = Build.DEVICE?.trim().orEmpty()
val systemVersion = Build.VERSION.RELEASE?.trim().orEmpty()
val systemVersion =
Build.VERSION.RELEASE
?.trim()
.orEmpty()
val locale = Locale.getDefault().toLanguageTag().trim()
val appVersion = BuildConfig.VERSION_NAME.trim()
val appBuild = BuildConfig.VERSION_CODE.toString()
return buildJsonObject {
put("deviceName", JsonPrimitive(model.ifEmpty { "Android" }))
put("modelIdentifier", JsonPrimitive(modelIdentifier.ifEmpty { listOf(manufacturer, model).filter { it.isNotEmpty() }.joinToString(" ") }))
put(
"modelIdentifier",
JsonPrimitive(modelIdentifier.ifEmpty { listOf(manufacturer, model).filter { it.isNotEmpty() }.joinToString(" ") }),
)
put("systemName", JsonPrimitive("Android"))
put("systemVersion", JsonPrimitive(systemVersion.ifEmpty { Build.VERSION.SDK_INT.toString() }))
put("appVersion", JsonPrimitive(appVersion.ifEmpty { "dev" }))
@@ -200,7 +194,17 @@ class DeviceHandler(
put(
"status",
JsonPrimitive(
if (hasAnySmsCapability(smsEnabled, canSendSms, smsSendGranted, smsReadGranted)) "granted" else "denied",
if (hasAnySmsCapability(
smsEnabled,
canSendSms,
smsSendGranted,
smsReadGranted,
)
) {
"granted"
} else {
"denied"
},
),
)
put("promptable", JsonPrimitive(isSmsPromptable(smsEnabled, canSendSms, smsSendGranted, smsReadGranted)))
@@ -367,24 +371,22 @@ class DeviceHandler(
return rawLevel.toDouble() / rawScale.toDouble()
}
private fun mapBatteryState(status: Int): String {
return when (status) {
private fun mapBatteryState(status: Int): String =
when (status) {
BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
BatteryManager.BATTERY_STATUS_FULL -> "full"
BatteryManager.BATTERY_STATUS_DISCHARGING, BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "unplugged"
else -> "unknown"
}
}
private fun mapChargingType(plugged: Int): String {
return when (plugged) {
private fun mapChargingType(plugged: Int): String =
when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "ac"
BatteryManager.BATTERY_PLUGGED_USB -> "usb"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "wireless"
BatteryManager.BATTERY_PLUGGED_DOCK -> "dock"
else -> "none"
}
}
private fun mapThermalState(powerManager: PowerManager?): String {
val thermal = powerManager?.currentThermalStatus ?: return "nominal"
@@ -394,7 +396,8 @@ class DeviceHandler(
PowerManager.THERMAL_STATUS_SEVERE -> "serious"
PowerManager.THERMAL_STATUS_CRITICAL,
PowerManager.THERMAL_STATUS_EMERGENCY,
PowerManager.THERMAL_STATUS_SHUTDOWN -> "critical"
PowerManager.THERMAL_STATUS_SHUTDOWN,
-> "critical"
else -> "nominal"
}
}
@@ -408,19 +411,24 @@ class DeviceHandler(
}
}
private fun permissionStateJson(granted: Boolean, promptableWhenDenied: Boolean) =
buildJsonObject {
put("status", JsonPrimitive(if (granted) "granted" else "denied"))
put("promptable", JsonPrimitive(!granted && promptableWhenDenied))
}
private fun hasPermission(permission: String): Boolean {
return (
ContextCompat.checkSelfPermission(appContext, permission) == PackageManager.PERMISSION_GRANTED
)
private fun permissionStateJson(
granted: Boolean,
promptableWhenDenied: Boolean,
) = buildJsonObject {
put("status", JsonPrimitive(if (granted) "granted" else "denied"))
put("promptable", JsonPrimitive(!granted && promptableWhenDenied))
}
private fun mapMemoryPressure(totalBytes: Long, availableBytes: Long, lowMemory: Boolean): String {
private fun hasPermission(permission: String): Boolean =
(
ContextCompat.checkSelfPermission(appContext, permission) == PackageManager.PERMISSION_GRANTED
)
private fun mapMemoryPressure(
totalBytes: Long,
availableBytes: Long,
lowMemory: Boolean,
): String {
if (totalBytes <= 0L) return if (lowMemory) "critical" else "unknown"
if (lowMemory) return "critical"
val freeRatio = availableBytes.toDouble() / totalBytes.toDouble()

View File

@@ -1,5 +1,9 @@
package ai.openclaw.app.node
import ai.openclaw.app.NotificationBurstLimiter
import ai.openclaw.app.SecurePrefs
import ai.openclaw.app.allowsPackage
import ai.openclaw.app.isWithinQuietHours
import android.app.Notification
import android.app.NotificationManager
import android.app.RemoteInput
@@ -8,10 +12,7 @@ import android.content.Context
import android.content.Intent
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import ai.openclaw.app.NotificationBurstLimiter
import ai.openclaw.app.SecurePrefs
import ai.openclaw.app.allowsPackage
import ai.openclaw.app.isWithinQuietHours
import androidx.core.content.edit
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
@@ -38,8 +39,8 @@ data class DeviceNotificationEntry(
val isClearable: Boolean,
)
internal fun DeviceNotificationEntry.toJsonObject(): JsonObject {
return buildJsonObject {
internal fun DeviceNotificationEntry.toJsonObject(): JsonObject =
buildJsonObject {
put("key", JsonPrimitive(key))
put("packageName", JsonPrimitive(packageName))
put("postTimeMs", JsonPrimitive(postTimeMs))
@@ -51,7 +52,6 @@ internal fun DeviceNotificationEntry.toJsonObject(): JsonObject {
category?.let { put("category", JsonPrimitive(it)) }
channelId?.let { put("channelId", JsonPrimitive(it)) }
}
}
data class DeviceNotificationSnapshot(
val enabled: Boolean,
@@ -77,9 +77,7 @@ data class NotificationActionResult(
val message: String? = null,
)
internal fun actionRequiresClearableNotification(kind: NotificationActionKind): Boolean {
return kind == NotificationActionKind.Dismiss
}
internal fun actionRequiresClearableNotification(kind: NotificationActionKind): Boolean = kind == NotificationActionKind.Dismiss
private object DeviceNotificationStore {
private val lock = Any()
@@ -193,8 +191,8 @@ class DeviceNotificationListenerService : NotificationListenerService() {
emitNotificationsChanged(payload)
}
private fun notificationChangedPayload(entry: DeviceNotificationEntry): String? {
return notificationChangedPayload(
private fun notificationChangedPayload(entry: DeviceNotificationEntry): String? =
notificationChangedPayload(
entry = entry,
change = "posted",
key = entry.key,
@@ -203,7 +201,6 @@ class DeviceNotificationListenerService : NotificationListenerService() {
isOngoing = entry.isOngoing,
isClearable = entry.isClearable,
)
}
private fun notificationChangedPayload(
entry: DeviceNotificationEntry?,
@@ -284,28 +281,30 @@ class DeviceNotificationListenerService : NotificationListenerService() {
private const val recentPackagesPref = "notifications.forwarding.recentPackages"
private const val legacyRecentPackagesPref = "notifications.recentPackages"
private const val recentPackagesLimit = 64
@Volatile private var activeService: DeviceNotificationListenerService? = null
@Volatile private var nodeEventSink: ((event: String, payloadJson: String?) -> Unit)? = null
private fun serviceComponent(context: Context): ComponentName {
return ComponentName(context, DeviceNotificationListenerService::class.java)
}
private fun serviceComponent(context: Context): ComponentName = ComponentName(context, DeviceNotificationListenerService::class.java)
fun setNodeEventSink(sink: ((event: String, payloadJson: String?) -> Unit)?) {
nodeEventSink = sink
}
private fun recentPackagesPrefs(context: Context) =
context.applicationContext.getSharedPreferences("openclaw.secure", Context.MODE_PRIVATE)
private fun recentPackagesPrefs(context: Context) = context.applicationContext.getSharedPreferences("openclaw.secure", Context.MODE_PRIVATE)
private fun migrateLegacyRecentPackagesIfNeeded(context: Context) {
val prefs = recentPackagesPrefs(context)
val hasNew = prefs.contains(recentPackagesPref)
val legacy = prefs.getString(legacyRecentPackagesPref, null)?.trim().orEmpty()
if (!hasNew && legacy.isNotEmpty()) {
prefs.edit().putString(recentPackagesPref, legacy).remove(legacyRecentPackagesPref).apply()
prefs.edit {
putString(recentPackagesPref, legacy)
remove(legacyRecentPackagesPref)
}
} else if (hasNew && prefs.contains(legacyRecentPackagesPref)) {
prefs.edit().remove(legacyRecentPackagesPref).apply()
prefs.edit { remove(legacyRecentPackagesPref) }
}
}
@@ -325,9 +324,10 @@ class DeviceNotificationListenerService : NotificationListenerService() {
return manager.isNotificationListenerAccessGranted(serviceComponent(context))
}
fun snapshot(context: Context, enabled: Boolean = isAccessEnabled(context)): DeviceNotificationSnapshot {
return DeviceNotificationStore.snapshot(enabled = enabled)
}
fun snapshot(
context: Context,
enabled: Boolean = isAccessEnabled(context),
): DeviceNotificationSnapshot = DeviceNotificationStore.snapshot(enabled = enabled)
fun requestServiceRebind(context: Context) {
runCatching {
@@ -335,7 +335,10 @@ class DeviceNotificationListenerService : NotificationListenerService() {
}
}
fun executeAction(context: Context, request: NotificationActionRequest): NotificationActionResult {
fun executeAction(
context: Context,
request: NotificationActionRequest,
): NotificationActionResult {
if (!isAccessEnabled(context)) {
return NotificationActionResult(
ok = false,
@@ -343,12 +346,13 @@ class DeviceNotificationListenerService : NotificationListenerService() {
message = "NOTIFICATIONS_DISABLED: enable notification access in system Settings",
)
}
val service = activeService
?: return NotificationActionResult(
ok = false,
code = "NOTIFICATIONS_UNAVAILABLE",
message = "NOTIFICATIONS_UNAVAILABLE: notification listener not connected",
)
val service =
activeService
?: return NotificationActionResult(
ok = false,
code = "NOTIFICATIONS_UNAVAILABLE",
message = "NOTIFICATIONS_UNAVAILABLE: notification listener not connected",
)
return service.executeActionInternal(request)
}
@@ -364,13 +368,16 @@ class DeviceNotificationListenerService : NotificationListenerService() {
if (normalized.isEmpty() || normalized == service.packageName) return
migrateLegacyRecentPackagesIfNeeded(service.applicationContext)
val prefs = recentPackagesPrefs(service.applicationContext)
val existing = prefs.getString(recentPackagesPref, null).orEmpty()
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() && it != normalized }
.take(recentPackagesLimit - 1)
val existing =
prefs
.getString(recentPackagesPref, null)
.orEmpty()
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() && it != normalized }
.take(recentPackagesLimit - 1)
val updated = listOf(normalized) + existing
prefs.edit().putString(recentPackagesPref, updated.joinToString(",")).apply()
prefs.edit { putString(recentPackagesPref, updated.joinToString(",")) }
}
}
@@ -393,12 +400,13 @@ class DeviceNotificationListenerService : NotificationListenerService() {
return when (request.kind) {
NotificationActionKind.Open -> {
val pendingIntent = sbn.notification.contentIntent
?: return NotificationActionResult(
ok = false,
code = "ACTION_UNAVAILABLE",
message = "ACTION_UNAVAILABLE: notification has no open action",
)
val pendingIntent =
sbn.notification.contentIntent
?: return NotificationActionResult(
ok = false,
code = "ACTION_UNAVAILABLE",
message = "ACTION_UNAVAILABLE: notification has no open action",
)
runCatching {
pendingIntent.send()
}.fold(

View File

@@ -1,11 +1,11 @@
package ai.openclaw.app.node
import ai.openclaw.app.protocol.OpenClawCalendarCommand
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
import ai.openclaw.app.protocol.OpenClawCanvasCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCapability
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawContactsCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
@@ -221,8 +221,8 @@ object InvokeCommandRegistry {
fun find(command: String): InvokeCommandSpec? = byNameInternal[command]
fun advertisedCapabilities(flags: NodeRuntimeFlags): List<String> {
return capabilityManifest
fun advertisedCapabilities(flags: NodeRuntimeFlags): List<String> =
capabilityManifest
.filter { spec ->
when (spec.availability) {
NodeCapabilityAvailability.Always -> true
@@ -233,12 +233,10 @@ object InvokeCommandRegistry {
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
NodeCapabilityAvailability.MotionAvailable -> flags.motionActivityAvailable || flags.motionPedometerAvailable
}
}
.map { it.name }
}
}.map { it.name }
fun advertisedCommands(flags: NodeRuntimeFlags): List<String> {
return all
fun advertisedCommands(flags: NodeRuntimeFlags): List<String> =
all
.filter { spec ->
when (spec.availability) {
InvokeCommandAvailability.Always -> true
@@ -252,7 +250,5 @@ object InvokeCommandRegistry {
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
}
}
.map { it.name }
}
}.map { it.name }
}

View File

@@ -2,10 +2,10 @@ package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.protocol.OpenClawCalendarCommand
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
import ai.openclaw.app.protocol.OpenClawCanvasCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawContactsCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
@@ -34,8 +34,8 @@ internal fun smsSearchAvailabilityError(
readSmsAvailable: Boolean,
smsFeatureEnabled: Boolean,
smsTelephonyAvailable: Boolean,
): GatewaySession.InvokeResult? {
return when (
): GatewaySession.InvokeResult? =
when (
classifySmsSearchAvailability(
readSmsAvailable = readSmsAvailable,
smsFeatureEnabled = smsFeatureEnabled,
@@ -43,14 +43,14 @@ internal fun smsSearchAvailabilityError(
)
) {
SmsSearchAvailabilityReason.Available,
SmsSearchAvailabilityReason.PermissionRequired -> null
SmsSearchAvailabilityReason.PermissionRequired,
-> null
SmsSearchAvailabilityReason.Unavailable ->
GatewaySession.InvokeResult.error(
code = "SMS_UNAVAILABLE",
message = "SMS_UNAVAILABLE: SMS not available on this device",
)
}
}
class InvokeDispatcher(
private val canvas: CanvasController,
@@ -82,7 +82,10 @@ class InvokeDispatcher(
private val motionActivityAvailable: () -> Boolean,
private val motionPedometerAvailable: () -> Boolean,
) {
suspend fun handleInvoke(command: String, paramsJson: String?): GatewaySession.InvokeResult {
suspend fun handleInvoke(
command: String,
paramsJson: String?,
): GatewaySession.InvokeResult {
val spec =
InvokeCommandRegistry.find(command)
?: return GatewaySession.InvokeResult.error(
@@ -151,7 +154,7 @@ class InvokeDispatcher(
} catch (err: Throwable) {
return GatewaySession.InvokeResult.error(
code = "INVALID_REQUEST",
message = err.message ?: "invalid A2UI payload"
message = err.message ?: "invalid A2UI payload",
)
}
withReadyA2ui {
@@ -186,9 +189,10 @@ class InvokeDispatcher(
OpenClawSystemCommand.Notify.rawValue -> systemHandler.handleSystemNotify(paramsJson)
// Photos command
ai.openclaw.app.protocol.OpenClawPhotosCommand.Latest.rawValue -> photosHandler.handlePhotosLatest(
paramsJson,
)
ai.openclaw.app.protocol.OpenClawPhotosCommand.Latest.rawValue ->
photosHandler.handlePhotosLatest(
paramsJson,
)
// Contacts command
OpenClawContactsCommand.Search.rawValue -> contactsHandler.handleContactsSearch(paramsJson)
@@ -216,14 +220,13 @@ class InvokeDispatcher(
}
}
private suspend fun withReadyA2ui(
block: suspend () -> GatewaySession.InvokeResult,
): GatewaySession.InvokeResult {
var a2uiUrl = a2uiHandler.resolveA2uiHostUrl()
?: return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
)
private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult {
var a2uiUrl =
a2uiHandler.resolveA2uiHostUrl()
?: return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
)
val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl)
if (!readyOnFirstCheck) {
if (!refreshNodeCanvasCapability()) {
@@ -247,10 +250,8 @@ class InvokeDispatcher(
return block()
}
private suspend fun withCanvasAvailable(
block: suspend () -> GatewaySession.InvokeResult,
): GatewaySession.InvokeResult {
return try {
private suspend fun withCanvasAvailable(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult =
try {
block()
} catch (_: Throwable) {
GatewaySession.InvokeResult.error(
@@ -258,10 +259,9 @@ class InvokeDispatcher(
message = "NODE_BACKGROUND_UNAVAILABLE: canvas unavailable",
)
}
}
private fun availabilityError(availability: InvokeCommandAvailability): GatewaySession.InvokeResult? {
return when (availability) {
private fun availabilityError(availability: InvokeCommandAvailability): GatewaySession.InvokeResult? =
when (availability) {
InvokeCommandAvailability.Always -> null
InvokeCommandAvailability.CameraEnabled ->
if (cameraEnabled()) {
@@ -309,7 +309,8 @@ class InvokeDispatcher(
)
}
InvokeCommandAvailability.ReadSmsAvailable,
InvokeCommandAvailability.RequestableSmsSearchAvailable ->
InvokeCommandAvailability.RequestableSmsSearchAvailable,
->
smsSearchAvailabilityError(
readSmsAvailable = readSmsAvailable(),
smsFeatureEnabled = smsFeatureEnabled(),
@@ -334,5 +335,4 @@ class InvokeDispatcher(
)
}
}
}
}

View File

@@ -30,7 +30,13 @@ internal object JpegSizeLimiter {
var width = initialWidth
var height = initialHeight
val clampedStartQuality = startQuality.coerceIn(minQuality, 100)
var best = JpegSizeLimiterResult(bytes = encode(width, height, clampedStartQuality), width = width, height = height, quality = clampedStartQuality)
var best =
JpegSizeLimiterResult(
bytes = encode(width, height, clampedStartQuality),
width = width,
height = height,
quality = clampedStartQuality,
)
if (best.bytes.size <= maxBytes) return best
repeat(maxScaleAttempts) {

View File

@@ -7,15 +7,19 @@ import android.location.Location
import android.location.LocationManager
import android.os.CancellationSignal
import androidx.core.content.ContextCompat
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.suspendCancellableCoroutine
import java.time.Instant
import java.time.format.DateTimeFormatter
class LocationCaptureManager(private val context: Context) {
data class Payload(val payloadJson: String)
class LocationCaptureManager(
private val context: Context,
) {
data class Payload(
val payloadJson: String,
)
suspend fun getLocation(
desiredProviders: List<String>,
@@ -98,15 +102,16 @@ class LocationCaptureManager(private val context: Context) {
val resolved =
providers.firstOrNull { manager.isProviderEnabled(it) }
?: throw IllegalStateException("LOCATION_UNAVAILABLE: no providers available")
val location = withTimeout(timeoutMs.coerceAtLeast(1)) {
suspendCancellableCoroutine<Location?> { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
manager.getCurrentLocation(resolved, signal, context.mainExecutor) { location ->
cont.resume(location) { _, _, _ -> }
val location =
withTimeout(timeoutMs.coerceAtLeast(1)) {
suspendCancellableCoroutine<Location?> { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
manager.getCurrentLocation(resolved, signal, context.mainExecutor) { location ->
cont.resume(location) { _, _, _ -> }
}
}
}
}
return location ?: throw IllegalStateException("LOCATION_UNAVAILABLE: no fix")
}
}

View File

@@ -1,11 +1,11 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.GatewaySession
import android.Manifest
import android.content.Context
import android.hardware.Sensor
@@ -8,17 +9,15 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.SystemClock
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.time.Instant
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.time.Instant
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.sqrt
@@ -67,9 +66,15 @@ internal interface MotionDataSource {
fun hasPermission(context: Context): Boolean
suspend fun activity(context: Context, request: MotionActivityRequest): MotionActivityRecord
suspend fun activity(
context: Context,
request: MotionActivityRequest,
): MotionActivityRecord
suspend fun pedometer(context: Context, request: MotionPedometerRequest): PedometerRecord
suspend fun pedometer(
context: Context,
request: MotionPedometerRequest,
): PedometerRecord
}
private object SystemMotionDataSource : MotionDataSource {
@@ -83,22 +88,27 @@ private object SystemMotionDataSource : MotionDataSource {
return sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
}
override fun hasPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
override fun hasPermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
override suspend fun activity(context: Context, request: MotionActivityRequest): MotionActivityRecord {
override suspend fun activity(
context: Context,
request: MotionActivityRequest,
): MotionActivityRecord {
if (!request.startISO.isNullOrBlank() || !request.endISO.isNullOrBlank()) {
throw IllegalArgumentException("MOTION_RANGE_UNAVAILABLE: historical activity range not supported on Android")
}
val sensorManager = context.getSystemService(SensorManager::class.java)
?: throw IllegalStateException("MOTION_UNAVAILABLE: sensor manager unavailable")
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
?: throw IllegalStateException("MOTION_UNAVAILABLE: accelerometer not available")
val sensorManager =
context.getSystemService(SensorManager::class.java)
?: throw IllegalStateException("MOTION_UNAVAILABLE: sensor manager unavailable")
val accelerometer =
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
?: throw IllegalStateException("MOTION_UNAVAILABLE: accelerometer not available")
val sample = readAccelerometerSample(sensorManager, accelerometer)
?: throw IllegalStateException("MOTION_UNAVAILABLE: no accelerometer sample")
val sample =
readAccelerometerSample(sensorManager, accelerometer)
?: throw IllegalStateException("MOTION_UNAVAILABLE: no accelerometer sample")
val end = Instant.now()
val start = end.minusSeconds(2)
val classification = classifyActivity(sample.averageDelta)
@@ -115,17 +125,23 @@ private object SystemMotionDataSource : MotionDataSource {
)
}
override suspend fun pedometer(context: Context, request: MotionPedometerRequest): PedometerRecord {
override suspend fun pedometer(
context: Context,
request: MotionPedometerRequest,
): PedometerRecord {
if (!request.startISO.isNullOrBlank() || !request.endISO.isNullOrBlank()) {
throw IllegalArgumentException("PEDOMETER_RANGE_UNAVAILABLE: historical pedometer range not supported on Android")
}
val sensorManager = context.getSystemService(SensorManager::class.java)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: sensor manager unavailable")
val stepCounter = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: step counting not supported")
val sensorManager =
context.getSystemService(SensorManager::class.java)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: sensor manager unavailable")
val stepCounter =
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: step counting not supported")
val steps = readStepCounter(sensorManager, stepCounter)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: no step counter sample")
val steps =
readStepCounter(sensorManager, stepCounter)
?: throw IllegalStateException("PEDOMETER_UNAVAILABLE: no step counter sample")
val bootMs = System.currentTimeMillis() - SystemClock.elapsedRealtime()
return PedometerRecord(
startISO = Instant.ofEpochMilli(max(0L, bootMs)).toString(),
@@ -143,7 +159,10 @@ private object SystemMotionDataSource : MotionDataSource {
)
@OptIn(InternalCoroutinesApi::class)
private suspend fun readStepCounter(sensorManager: SensorManager, sensor: Sensor): Int? {
private suspend fun readStepCounter(
sensorManager: SensorManager,
sensor: Sensor,
): Int? {
val sample =
withTimeoutOrNull(1200L) {
suspendCancellableCoroutine<Float?> { cont ->
@@ -156,7 +175,10 @@ private object SystemMotionDataSource : MotionDataSource {
sensorManager.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
override fun onAccuracyChanged(
sensor: Sensor?,
accuracy: Int,
) = Unit
}
val registered = sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
if (!registered) {
@@ -194,17 +216,21 @@ private object SystemMotionDataSource : MotionDataSource {
sumDelta += abs(magnitude - SensorManager.GRAVITY_EARTH.toDouble())
count += 1
if (count >= ACCELEROMETER_SAMPLE_TARGET) {
val result = AccelerometerSample(
samples = count,
averageDelta = sumDelta / count,
)
val result =
AccelerometerSample(
samples = count,
averageDelta = sumDelta / count,
)
val token = cont.tryResume(result) ?: return
cont.completeResume(token)
sensorManager.unregisterListener(this)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
override fun onAccuracyChanged(
sensor: Sensor?,
accuracy: Int,
) = Unit
}
val registered = sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
if (!registered) {
@@ -217,15 +243,17 @@ private object SystemMotionDataSource : MotionDataSource {
return sample
}
private fun classifyActivity(averageDelta: Double): String {
return when {
private fun classifyActivity(averageDelta: Double): String =
when {
averageDelta <= 0.55 -> "stationary"
averageDelta <= 1.80 -> "walking"
else -> "running"
}
}
private fun classifyConfidence(samples: Int, averageDelta: Double): String {
private fun classifyConfidence(
samples: Int,
averageDelta: Double,
): String {
if (samples < 6) return "low"
if (samples >= 14 && averageDelta > 0.4) return "high"
return "medium"

View File

@@ -0,0 +1,98 @@
package ai.openclaw.app.node
import android.os.Build
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
internal object NodePresenceAliveBeacon {
const val EVENT_NAME: String = "node.presence.alive"
const val MIN_SUCCESS_INTERVAL_MS: Long = 10 * 60 * 1000
private const val MAX_RESPONSE_JSON_CHARS: Int = 16 * 1024
enum class Trigger(
val rawValue: String,
) {
Background("background"),
SilentPush("silent_push"),
BackgroundAppRefresh("bg_app_refresh"),
SignificantLocation("significant_location"),
Manual("manual"),
Connect("connect"),
}
data class ResponsePayload(
val ok: Boolean?,
val event: String?,
val handled: Boolean?,
val reason: String?,
)
private val json = Json { ignoreUnknownKeys = true }
fun shouldSkipRecentSuccess(
nowMs: Long,
lastSuccessAtMs: Long?,
minIntervalMs: Long = MIN_SUCCESS_INTERVAL_MS,
): Boolean {
val last = lastSuccessAtMs ?: return false
if (last <= 0) return false
val elapsed = nowMs - last
return elapsed >= 0 && elapsed < minIntervalMs
}
fun androidPlatformLabel(): String {
val release =
Build.VERSION.RELEASE
?.trim()
.orEmpty()
.ifEmpty { "unknown" }
return "Android $release (SDK ${Build.VERSION.SDK_INT})"
}
fun makePayloadJson(
trigger: Trigger,
sentAtMs: Long,
displayName: String,
version: String,
platform: String,
deviceFamily: String?,
modelIdentifier: String?,
pushTransport: String? = null,
): String =
buildJsonObject {
put("trigger", JsonPrimitive(trigger.rawValue))
put("sentAtMs", JsonPrimitive(sentAtMs))
put("displayName", JsonPrimitive(displayName))
put("version", JsonPrimitive(version))
put("platform", JsonPrimitive(platform))
deviceFamily?.trim()?.takeIf { it.isNotEmpty() }?.let { put("deviceFamily", JsonPrimitive(it)) }
modelIdentifier?.trim()?.takeIf { it.isNotEmpty() }?.let { put("modelIdentifier", JsonPrimitive(it)) }
pushTransport?.trim()?.takeIf { it.isNotEmpty() }?.let { put("pushTransport", JsonPrimitive(it)) }
}.toString()
fun decodeResponse(payloadJson: String?): ResponsePayload? {
val raw = payloadJson?.trim()?.takeIf { it.isNotEmpty() } ?: return null
if (raw.length > MAX_RESPONSE_JSON_CHARS) return null
val obj =
try {
json.parseToJsonElement(raw).asObjectOrNull()
} catch (_: Throwable) {
null
} ?: return null
return ResponsePayload(
ok = parseJsonBooleanFlag(obj, "ok"),
event = parseJsonString(obj, "event"),
handled = parseJsonBooleanFlag(obj, "handled"),
reason = parseJsonString(obj, "reason"),
)
}
fun sanitizeReasonForLog(raw: String?): String {
val value = raw?.trim()?.takeIf { it.isNotEmpty() } ?: "unsupported"
return value
.map { ch -> if (ch.isISOControl()) ' ' else ch }
.joinToString("")
.take(200)
}
}

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