Compare commits

...

577 Commits

Author SHA1 Message Date
Keshav's Bot
dde017027c fix(codex): gate profiler timing and startup setup 2026-05-26 19:02:13 +01:00
Vincent Koc
2b63eb2825 fix(e2e): bound corrupt plugin update runs 2026-05-26 19:54:05 +02:00
Peter Steinberger
6930538500 ci: require codex profiles for live probes 2026-05-26 18:51:43 +01:00
Peter Steinberger
cd46057b90 docs: clarify inline comment guidance 2026-05-26 18:49:29 +01:00
Peter Steinberger
8c575bd3c8 docs: update changelog for landed sweep fixes 2026-05-26 18:41:00 +01:00
Fermin Quant
598aad4f66 fix(agents): disclose scoped session list results (#86944)
* fix(agents): disclose scoped session list results

* fix(agents): clarify scoped session count warning
2026-05-26 18:40:36 +01:00
Andy Ye
1fd8de8495 fix(telegram): treat ENETDOWN as transient network failure (#86762) 2026-05-26 18:40:31 +01:00
Vincent Koc
564e0bb5c1 fix(mac): harden package script safety 2026-05-26 19:40:16 +02:00
Vincent Koc
c867ecb136 fix(ci): kill wedged checkout fetches 2026-05-26 19:38:34 +02:00
Peter Steinberger
9fd8158c06 ci: restore codex replay live probe contract 2026-05-26 18:35:56 +01:00
Eva
7a147419db fix(codex): preserve oversized native thread reuse
Reworks the Codex app-server native thread reuse guard so OpenClaw no longer adds a user-facing token config. Token clearing now prefers Codex's reported model context window, falls back to a high internal recovery fuse, and preserves context-engine thread-bootstrap reuse while keeping byte guard behavior intact.

Verification:
- `fnm exec --using v24.15.0 -- node scripts/run-vitest.mjs run extensions/codex/src/app-server/run-attempt.test.ts extensions/codex/src/app-server/run-attempt.context-engine.test.ts --reporter=dot --pool=forks --no-file-parallelism`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local --base origin/main`
- Testbox `check:changed`: `tbx_01ksjm1hy7mfrc5bebzyckqdew`, GitHub Actions run https://github.com/openclaw/openclaw/actions/runs/26463150977, exit 0
- PR CI green after rerunning unrelated `checks-node-agentic-agents` flake and stuck OpenGrep scan

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
2026-05-26 18:33:59 +01:00
Vincent Koc
a5eee8f1c6 fix(scripts): detect timed changed gates 2026-05-26 19:19:26 +02:00
Peter Steinberger
3c6fd49d74 ci: stop waiting for nonexistent capability restart wake 2026-05-26 18:15:16 +01:00
Vincent Koc
e8f584e400 fix(e2e): route plugin update through timeout helper 2026-05-26 19:11:09 +02:00
Peter Steinberger
7e6837bc07 fix: respect root options in startup guards (#86927) 2026-05-26 18:08:51 +01:00
Peter Steinberger
0ec29289c6 fix: tighten CLI utility failure handling (#86918)
* fix: tighten cli utility failure handling

* fix: preserve completion install error cause

* fix: keep update completion refresh best effort
2026-05-26 18:08:44 +01:00
Peter Steinberger
82dae95c76 fix: preserve config and hook contracts (#86911) 2026-05-26 18:08:39 +01:00
Peter Steinberger
c147e27f5a fix: tighten small runtime parsing guards (#86909) 2026-05-26 18:08:33 +01:00
Vincent Koc
081e29595e fix(ci): kill timed tui pty test runs 2026-05-26 18:55:47 +02:00
Onur Solmaz
6c18c212e9 fix(logging): preserve env placeholders during redaction
* fix(logging): preserve env placeholders during redaction

* fix(logging): honor custom redaction patterns

* fix(logging): preserve generic env placeholders

---------

Co-authored-by: Onur Solmaz <onur@Onurs-MacBook-Pro.local>
2026-05-27 00:49:34 +08:00
lukeboyett
9e43d0327f fix(memory-core): avoid per-file watcher FD fan-out for memory directories (#86701)
Merged via squash.

Prepared head SHA: e27c28a3a1
Co-authored-by: lukeboyett <46942646+lukeboyett@users.noreply.github.com>
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
Reviewed-by: @osolmaz
2026-05-27 00:48:22 +08:00
Peter Steinberger
5535eef6b0 fix: use current config sdk contract in feishu doctor 2026-05-26 17:45:24 +01:00
Michael Appel
84b9704ccc Validate wide-area DNS zone domains [AI] (#84136)
* fix: validate wide-area dns domains

* addressing codex review

* fix(dns-cli): throw explicit DNS-name error on invalid --domain

resolveWideAreaDiscoveryDomain catches the validation error from
normalizeWideAreaDomain and returns null, so dns setup --domain foo/bar
fell through to the "No wide-area domain configured" branch instead of
surfacing the invalid-domain diagnostic. Validate explicit CLI/config
input directly so the user-facing setup command reports the actual
problem; preserve the resolver's silent env-fallback semantics for the
background callers that depend on graceful degradation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gateway): lock in graceful degrade on invalid wide-area config

Drive startGatewayDiscovery through the real resolveWideAreaDiscoveryDomain
with wideAreaDiscoveryDomain: "foo/bar" so the test exercises the actual
swallow-and-return-null path. Asserts the operator-facing warning is
logged, writeWideAreaGatewayZone is never called, and startup completes
without throwing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gateway): type resolveWideAreaDiscoveryDomain mock to match real signature

vi.fn(() => "openclaw.internal.") inferred the mock as `() => string`, so
mockImplementationOnce(realResolver) tripped tsgo:core:test with TS2345.
Apply the same vi.fn<typeof ...>(...) pattern the file already uses for
writeWideAreaGatewayZone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(changelog): note dns validation fix

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
2026-05-26 09:43:58 -07:00
Peter Steinberger
27359ec417 ci: stabilize release live QA gates 2026-05-26 17:41:30 +01:00
Peter Steinberger
cf21c8abcb ci: harden live release gates 2026-05-26 17:41:30 +01:00
Peter Steinberger
c84f61cd2e ci: normalize Windows toolcache Node paths 2026-05-26 17:41:30 +01:00
Peter Steinberger
fdb7848a7c chore: remove stale codex test conversion 2026-05-26 17:40:44 +01:00
Peter Steinberger
496fd8f853 perf: cache read-only channel resolution 2026-05-26 17:40:44 +01:00
Vincent Koc
373b3bfe54 fix(test): explain missing vitest dependency 2026-05-26 18:33:50 +02:00
Vincent Koc
d5bf325126 fix(e2e): kill timed docker scenario runners 2026-05-26 18:31:38 +02:00
Peter Steinberger
645cbf6c33 fix: add transcripts tool display metadata 2026-05-26 17:28:23 +01:00
Peter Steinberger
12b81d8978 docs: update changelog for landed fixes 2026-05-26 17:22:19 +01:00
Neerav Makwana
06afc57102 fix(agents): route btw through embedded stream resolver (#86312) 2026-05-26 17:21:38 +01:00
狼哥
c7821bd2a8 fix(telegram): treat targeted bot commands as mentions (#86553) 2026-05-26 17:21:33 +01:00
Vincent Koc
9ced76a4bb fix(e2e): route doctor switch commands through timeout helper 2026-05-26 18:19:44 +02:00
Shadow
7671068daf fix(ci): evaluate duplicate proof sections 2026-05-26 11:18:42 -05:00
Peter Steinberger
ead847f606 fix: ignore other codex thread completions 2026-05-26 17:16:17 +01:00
Peter Steinberger
b7c461af7b fix(feishu): repair stale channel state
Closes #74237.
Recreates #74397 locally because the fork disallows maintainer edits.

Co-authored-by: Lightningxxl <yuanhangxurobin@gmail.com>
2026-05-26 17:10:34 +01:00
Peter Steinberger
0973a7e4e4 fix: remove stale image provider assertions 2026-05-26 17:04:09 +01:00
Peter Steinberger
d001d35ea2 fix: accept trailing fuzzy voice wake questions 2026-05-26 16:59:05 +01:00
Vincent Koc
d6fcb562f4 fix(podman): bound setup image builds 2026-05-26 17:58:42 +02:00
Vincent Koc
6118f3f615 fix(podman): kill timed container launches 2026-05-26 17:47:25 +02:00
Vincent Koc
fb853de554 fix(scripts): preserve native pnpm exec paths 2026-05-26 17:36:48 +02:00
Vincent Koc
e96cde7e14 fix(ci): bound docker pull smoke steps 2026-05-26 17:28:37 +02:00
Vincent Koc
5ef812293b fix(codex): bridge cli api-key auth into app-server 2026-05-26 17:19:50 +02:00
Peter Steinberger
0f605ee003 fix: update Discord voice to libopus-wasm 0.1.0
Updates Discord voice Opus callers to the published libopus-wasm 0.1.0 API, pins the Discord plugin dependency and lockfiles to that release, keeps the package freshness exception version-scoped, treats expected Discord receive-stream premature closes as normal stream ends, and includes routed OpenClaw transcript roots for local PR transcript discovery.\n\nProof: npm view libopus-wasm@0.1.0; pnpm install --lockfile-only --filter @openclaw/discord; Node encode/decode smoke with pkg 0.1.0 decoded=3840; node scripts/run-vitest.mjs extensions/discord/src/voice/audio.test.ts extensions/discord/src/voice/receive-recovery.test.ts; git diff --check; autoreview clean; live tmux gateway on e0fa3e3 joined Discord voice and processed realtime audio without decoder.decode or Premature close warning spam.
2026-05-26 16:17:53 +01:00
Vincent Koc
e89afa6afa fix(e2e): kill timed docker helper commands 2026-05-26 17:16:20 +02:00
Vincent Koc
dc0d4c263e fix(e2e): kill timed live docker runs 2026-05-26 17:03:57 +02:00
Vincent Koc
d54c90699f fix(ci): kill timed website installer docker steps 2026-05-26 16:51:43 +02:00
Vincent Koc
4ff5a6152c fix(scripts): trim macOS node bootstrap 2026-05-26 16:42:44 +02:00
Vincent Koc
cf6f9ad8a3 fix(ci): kill timed install smoke docker steps 2026-05-26 16:36:18 +02:00
Nimrod Gutman
19e4c37c37 feat(ios): show Talk voice mode (#86798)
Merged via squash.

Prepared head SHA: bd24da3f3b
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-26 17:31:31 +03:00
Vincent Koc
35310dce8c fix(setup): kill timed image pulls when supported 2026-05-26 16:19:34 +02:00
Vincent Koc
8685dbd547 fix(test): default Vitest stall watchdog 2026-05-26 16:17:13 +02:00
Vincent Koc
d1c8f09b00 fix(ci): bound crabbox hydrate downloads 2026-05-26 15:57:01 +02:00
Marvinthebored
42ba297b0a fix(control-ui): guard stale overview usage refresh
Guard loadUsage in the Control UI overview secondary refresh so stale overview loads do not start the expensive usage.cost RPC after the user has navigated away. Active overview usage loading is preserved.

Fixes #86392.
Thanks @Marvinthebored for the report, live gateway proof, and patch.

Verification:
- CI=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=120000 fnm exec --using v24.15.0 -- node scripts/run-vitest.mjs run ui/src/ui/app-settings.refresh-active-tab.node.test.ts --reporter=dot --pool=forks --no-file-parallelism
- GitHub PR checks green on d52d8d10da, including Real behavior proof and checks-node-core-ui.

Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-26 14:54:38 +01:00
Vincent Koc
4d4e2ec256 fix(qa): require genai otel model spans (#86920) 2026-05-26 14:51:50 +01:00
Peter Steinberger
cac0b2db18 refactor: move transcripts into core
Move meeting notes into core transcripts, remove the bundled meeting-notes plugin/API, and require explicit transcripts.enabled before exposing the recording-capable tool.
2026-05-26 14:51:11 +01:00
Peter Steinberger
45feb37b13 fix(message-tool): hydrate structured reply attachments
Fix outbound message actions so structured attachments[] media participates in existing sandbox, local-root, and hydration checks. Single-attachment actions select structured attachments only when no top-level or plugin media source wins, while send collects all structured attachments. Proof: git diff --check; pnpm tsgo:core && pnpm tsgo:test:src; direct selector/hydration probe; autoreview clean.
2026-05-26 14:50:32 +01:00
Vincent Koc
ce61d224d8 fix(e2e): kill timed npm install process groups 2026-05-26 15:49:29 +02:00
Vincent Koc
c38b5033e6 fix(ci): kill timed workflow process groups 2026-05-26 15:40:44 +02:00
Vincent Koc
0cca7861c1 fix(e2e): kill timed setup process groups 2026-05-26 15:31:56 +02:00
Vincent Koc
d0dd8b8a41 fix(e2e): bound resource-sampled docker runs 2026-05-26 15:09:14 +02:00
Vincent Koc
295b5ea9ab fix(mac): fail closed on restart gateway check 2026-05-26 15:05:16 +02:00
Vincent Koc
8c7f226401 fix(e2e): time out live docker setup installs 2026-05-26 14:58:45 +02:00
Peter Steinberger
e37ac22fdd ci: resolve major node fallback versions 2026-05-26 13:47:19 +01:00
Peter Steinberger
50c7d780dc ci: add node download fallback 2026-05-26 13:47:19 +01:00
Peter Steinberger
4c6aeb9bb2 ci: use local node toolcache setup 2026-05-26 13:47:19 +01:00
Vincent Koc
9777526eaa fix(e2e): bound docker npm install phases 2026-05-26 14:46:05 +02:00
Peter Steinberger
84e4bff73b ci: restore pnpm store cache with corepack 2026-05-26 13:39:00 +01:00
Peter Steinberger
13f72e4102 ci: avoid pnpm setup action download 2026-05-26 13:39:00 +01:00
Vincent Koc
a17ac3ec9d fix(e2e): time out telegram package installs 2026-05-26 14:36:20 +02:00
Peter Steinberger
e549d0c235 ci: avoid unconditional bun action download 2026-05-26 13:31:21 +01:00
Peter Steinberger
8d6a6e9f89 ci: use unauthenticated workflow fetches 2026-05-26 13:28:32 +01:00
Peter Steinberger
df13d3a724 ci: avoid token-backed read checkouts 2026-05-26 13:28:32 +01:00
Vincent Koc
a07dc3896b fix(e2e): time out package npm installs 2026-05-26 14:18:58 +02:00
Peter Steinberger
30e59b4090 test: speed up slow CI regressions 2026-05-26 13:13:19 +01:00
Vincent Koc
dfe94ff048 fix(release): fail closed on cross-os agent turns 2026-05-26 14:06:02 +02:00
Vincent Koc
419178b9bc fix(e2e): reject corrupt plugin update false greens 2026-05-26 13:49:02 +02:00
Peter Steinberger
efebf6bfcf fix(qa): preserve corrupt auth profile files 2026-05-26 12:42:50 +01:00
Peter Steinberger
cb34175dfd fix(matrix): reject malformed integer cli values 2026-05-26 12:42:43 +01:00
Peter Steinberger
884d346999 fix(canvas): reject invalid snapshot formats 2026-05-26 12:42:36 +01:00
Peter Steinberger
13c6a3332c fix(browser): reject invalid wait load states 2026-05-26 12:42:24 +01:00
Vincent Koc
a3bb4fe814 fix(ci): time out website installer docker runs 2026-05-26 13:29:35 +02:00
adupdev
31a8fe7462 fix(discord): gate native built-in UI before owner auth
Fixes #86654
2026-05-26 12:28:32 +01:00
Vincent Koc
92fb79ee69 fix(ci): fail Testbox changed-check delegation 2026-05-26 13:24:42 +02:00
Vincent Koc
30c4489af4 fix(ci): time out install smoke docker runs 2026-05-26 13:22:20 +02:00
Vincent Koc
94a04e1aa6 ci(release): retry transient GitHub API suspension errors (#86859) 2026-05-26 12:17:38 +01:00
Vincent Koc
8307e2f762 fix(podman): time out detached launches 2026-05-26 13:12:53 +02:00
Peter Steinberger
5b49433535 Auto-scale live tool result caps (#86857)
* fix: auto-scale live tool result cap

* fix: auto-scale live tool result cap
2026-05-26 12:11:31 +01:00
Vincent Koc
c2b1d20c25 fix(podman): time out setup image pulls 2026-05-26 12:56:35 +02:00
Peter Steinberger
18ff19e043 perf: use typed arrays for audio codec loops (#86856) 2026-05-26 11:51:21 +01:00
Vincent Koc
f0599fddac fix(e2e): time out live image pulls 2026-05-26 12:48:38 +02:00
Vincent Koc
fe9f28f520 fix(mac): require dist dSYM artifacts 2026-05-26 12:46:46 +02:00
Peter Steinberger
71e7a1fd7d docs: update changelog for testbox delegation 2026-05-26 11:44:06 +01:00
Vincent Koc
92082723f7 fix(e2e): time out installer smoke containers 2026-05-26 12:36:06 +02:00
Peter Steinberger
e20b8d70a6 fix: simplify testbox changed-check delegation 2026-05-26 11:33:09 +01:00
Vincent Koc
198d0a56d3 fix(mac): require packaged app resources 2026-05-26 12:27:47 +02:00
Peter Steinberger
11512b1257 test: update docker stats helper expectations 2026-05-26 11:26:18 +01:00
Vincent Koc
d1f2eb0709 fix(e2e): time out live Docker runs 2026-05-26 12:24:13 +02:00
Vincent Koc
e8cb2b5ab3 fix(mac): remove unused codesign entitlements 2026-05-26 12:19:42 +02:00
Vincent Koc
dcf0941cd6 fix(docker): time out setup image pulls 2026-05-26 12:14:18 +02:00
Vincent Koc
da16a966c3 fix(mac): fail closed on missing staple app 2026-05-26 12:13:04 +02:00
Vincent Koc
4ebc13abe1 fix(qa-slack): preserve failure debug artifacts 2026-05-26 11:09:52 +01:00
Vincent Koc
f1ceed94db fix(e2e): time out standalone Docker smokes 2026-05-26 12:07:04 +02:00
Vincent Koc
68f877ef66 fix(mac): clean codesign entitlement temps 2026-05-26 12:06:18 +02:00
Vincent Koc
1c5b8353d6 fix(e2e): time out install smoke Docker copies 2026-05-26 11:57:32 +02:00
Vincent Koc
7aedff8fbb fix(mac): fail closed on dmg plist reads 2026-05-26 11:54:47 +02:00
Vincent Koc
f2ad94ec9a fix(e2e): route gateway network client through Docker helper 2026-05-26 11:49:17 +02:00
Vincent Koc
8e110a2122 fix(mac): fail closed on dist plist reads 2026-05-26 11:44:05 +02:00
Peter Steinberger
4c8e9da033 test(codex): widen app-server wait timeout type 2026-05-26 10:42:30 +01:00
Vincent Koc
55af31e0c6 fix(e2e): time out Docker image reuse probes 2026-05-26 11:42:02 +02:00
Peter Steinberger
4f1cd8eb00 docs: clarify compatibility defaults 2026-05-26 10:39:21 +01:00
Vincent Koc
e295c86dbc fix(e2e): route named container cleanup through helper 2026-05-26 11:32:25 +02:00
Vincent Koc
91080fde68 fix(mac): fail closed on plist stamp errors 2026-05-26 11:28:51 +02:00
Vincent Koc
4838e704a0 fix(e2e): route focused docker smokes through run helper 2026-05-26 11:24:15 +02:00
Alex Knight
21aebd5fbc fix(mattermost): tag typed text slash control commands
Tag authorized Mattermost typed text-slash control commands with CommandSource: text so existing explicit-command source-reply delivery bypasses message_tool_only suppression for /new, /reset, ACP reset, and soft-reset acknowledgement replies.

Remove the normal PR changelog edit flagged by review and keep release-note context in the PR body/squash message. Tighten the regression test to exercise the leading-space Mattermost text-post path used to bypass native slash handling and assert the normalized command body.

Local proof: node scripts/run-vitest.mjs extensions/mattermost/src/mattermost/monitor.inbound-system-event.test.ts src/auto-reply/command-turn-context.test.ts src/auto-reply/reply/source-reply-delivery-mode.test.ts src/auto-reply/reply/commands-reset-hooks.test.ts; git diff --check origin/main..HEAD; oxfmt check; autoreview clean.

CI: PR run 26443271650 passed relevant checks. Ignored check-test-types failure because the exact same extensions/codex/src/app-server/run-attempt.test.ts TS2345 failure is already present on main run 26442926352 at the PR base.

Fixes #86664.
2026-05-26 19:18:41 +10:00
Vincent Koc
29919cbec5 fix(e2e): route sampled docker runs through helpers 2026-05-26 11:14:41 +02:00
Vincent Koc
90bcec9fa4 fix(e2e): clean package docker artifacts on setup failure 2026-05-26 11:06:29 +02:00
Peter Steinberger
0e733795f4 ci(release): include performance run in validation manifest 2026-05-26 10:03:41 +01:00
Vincent Koc
99032f0354 test(e2e): harden release media memory smoke 2026-05-26 10:58:49 +02:00
Vincent Koc
f63754b314 fix(e2e): clean package onboarding artifacts 2026-05-26 10:50:23 +02:00
Vincent Koc
b34e1b32d8 fix(e2e): honor Docker harness run timeouts 2026-05-26 10:42:34 +02:00
Omar Shahine
9434228cdc fix(imessage): dedupe accounts sharing the local Messages source (#86705)
Merged via squash.

Prepared head SHA: fcfe97d7c8
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-05-26 01:39:12 -07:00
Vincent Koc
21000a3da7 fix(scripts): detect shell-wrapped changed gates 2026-05-26 10:24:22 +02:00
Vincent Koc
3f6b63aa1d fix(codex): preserve sandbox bootstrap path style 2026-05-26 10:21:47 +02:00
Peter Steinberger
c5530c798c perf: skip canonical session migration parses 2026-05-26 09:18:14 +01:00
Vincent Koc
d3bbfa1f5a fix(e2e): clean skill install package mounts 2026-05-26 10:10:28 +02:00
Vincent Koc
a5653c0ce9 fix(e2e): clean Codex plugin live artifacts 2026-05-26 10:02:05 +02:00
Vincent Koc
b93cee45d0 fix(e2e): clean sampled Docker logs on failure 2026-05-26 09:52:42 +02:00
Peter Steinberger
3548cff14b refactor: migrate validators to TypeBox (#86639)
* refactor: migrate validators to typebox

* fix: preserve json schema resource refs

* chore: clean schema preflight recursion

* refactor: remove lobster ajv shim

* fix: support schema array refs

* fix: validate schema dependencies

* fix: preserve schema contract checks

* fix: support same-document schema refs

* fix: preserve untyped map defaults

* fix: preserve schema default semantics

* test: avoid thenable schema literals

* test: build conditional schema key

* fix: defer resource id refs to typebox

* fix: reject invalid schema enum metadata

* fix: preserve default branch semantics

* fix: resolve schema resource refs

* fix: narrow conditional default fallback

* fix: preserve uri format validation

* fix: preserve validator compatibility

* test: avoid ajv cache lint violation

* fix: preserve typebox validation diagnostics

* fix: validate defaulted conditional schemas

* fix: normalize mcp draft schemas

* fix: preserve tuple schema defaults

* fix: resolve relative schema refs

* fix: scope typebox format semantics

* fix: align conditional format defaults

* fix: decode schema pointer refs

* fix: filter grouped secretref diagnostics

* fix: preserve default conditional compatibility

* fix: preserve nullable schema compatibility

* fix: settle defaults before conditionals

* fix: preserve default validation invariants

* fix: validate dynamic schema refs

* fix: reject malformed nullable schemas
2026-05-26 08:45:28 +01:00
Vincent Koc
b377618fae fix(e2e): clean package mount tarballs 2026-05-26 09:43:40 +02:00
Vincent Koc
437a9e9171 fix(scripts): format auth expiries on macos 2026-05-26 09:32:24 +02:00
Vincent Koc
abc7b7b331 fix(e2e): clean functional Docker build inputs 2026-05-26 09:29:57 +02:00
Omar Shahine
2e17003165 Fix iMessage image attachment roots (#86569)
* fix imessage image attachment roots

* fix media tool inbound wildcard roots

* docs(changelog): add iMessage image attachment root fix entry for #86569

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-26 00:22:12 -07:00
Peter Steinberger
918472a27b chore(release): refresh plugin sdk api baseline 2026-05-26 08:21:07 +01:00
Vincent Koc
4a1d772f3d fix(e2e): fail on invalid test state payloads 2026-05-26 09:15:06 +02:00
Peter Steinberger
4beadbf951 ci(release): apply exact extension batch excludes 2026-05-26 08:08:50 +01:00
Vincent Koc
6c5b39291f fix(installer): reject invalid shell options 2026-05-26 08:51:50 +02:00
Peter Steinberger
3b023e9bdb ci(release): pass vitest batch options before roots 2026-05-26 07:50:52 +01:00
Peter Steinberger
a3cd90fb5a ci(release): exclude codex app-server integration from plugin prerelease 2026-05-26 07:36:48 +01:00
Omar Shahine
17f7ef5c0f fix(imessage): send group media via attachment command (#86770)
* fix(imessage): send group media via attachment command

* fix(imessage): preserve media rpc fallback

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-25 23:31:27 -07:00
Vincent Koc
41eef4a796 test(e2e): assert release upgrade installs candidate 2026-05-26 08:27:23 +02:00
Peter Steinberger
a46556a6c2 ci(release): serialize plugin prerelease extension batch 2026-05-26 07:15:56 +01:00
Vincent Koc
81f62a689b fix(scripts): add docker e2e scheduler help 2026-05-26 08:07:36 +02:00
Peter Steinberger
083377adb8 test(codex): wait for diagnostic event locally 2026-05-26 06:53:40 +01:00
Vincent Koc
4b03e07294 test(e2e): assert release plugin uninstall removes files 2026-05-26 07:53:20 +02:00
Vincent Koc
16d137dce6 test(telegram): use platform temp path in bot harness 2026-05-26 07:49:19 +02:00
Omar Shahine
3452382cc0 fix(imessage): seed direct DM history (#86706)
* fix(imessage): seed direct DM history

* docs(imessage): clarify DM history override seeding

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-25 22:38:32 -07:00
Peter Steinberger
11b1b7c888 test(codex): complete diagnostic turn explicitly 2026-05-26 06:32:54 +01:00
Vincent Koc
5c3fb1f9d1 test(scripts): make run-vitest test Windows-safe 2026-05-26 07:28:34 +02:00
Peter Steinberger
c04c03f8e9 test: restore auth regression coverage 2026-05-26 06:23:13 +01:00
Vincent Koc
505aca9ef7 fix(test): reject missing explicit vitest files 2026-05-26 07:06:55 +02:00
Vincent Koc
5174d9744e test(plugins): canonicalize plugin install assertion paths 2026-05-26 07:04:41 +02:00
clawsweeper[bot]
23e9bc8c0b fix(diagnostics): track model stream progress (#86757)
Summary:
- The PR updates diagnostics to mark streamed model chunks as run progress, keeps silent model calls abortable after the stuck-session timeout, and adds regression coverage for stream progress and recovery behavior.
- PR surface: Source +54, Tests +229. Total +283 across 6 files.
- Reproducibility: yes. at source level: current main tracks model-call start/end activity but streamed chunks ... covery keys on stale lastProgressAgeMs. I did not run a live local-provider repro in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(diagnostics): track model stream progress
- PR branch already contained follow-up commit before automerge: test(diagnostics): cover silent local model aborts
- PR branch already contained follow-up commit before automerge: fix(diagnostics): skip stream progress when disabled

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

Prepared head SHA: fcc74d9869
Review: https://github.com/openclaw/openclaw/pull/86757#issuecomment-4540111930

Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-26 04:47:11 +00:00
Andy Ye
711e963723 Preserve runtime external auth snapshots (#85558)
Summary:
- The PR adds runtime-only external OAuth provenance to auth-profile stores, updates save/merge/read paths to  ... e profiles in active snapshots while filtering disk persistence, and expands auth-profile regression tests.
- PR surface: Source +381, Tests +974. Total +1355 across 8 files.
- Reproducibility: yes. from source: current main writes the disk-filtered localStore into an existing runtime ... tches the reported credential drop path. I did not run a failing current-main repro in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Preserve runtime external auth snapshots

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

Prepared head SHA: a73074ed45
Review: https://github.com/openclaw/openclaw/pull/85558#issuecomment-4523577269

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-26 04:41:59 +00:00
Vincent Koc
7db4b3db41 fix(test): prepare macos runner tmpdir 2026-05-26 06:24:46 +02:00
Peter Steinberger
c14c043be7 test(agents): stabilize yielded exec timeout test 2026-05-26 05:21:59 +01:00
Peter Steinberger
3bb4be23c0 test: stabilize media fallback and background timeout tests 2026-05-26 05:19:50 +01:00
Liz Zhang
72a7d6a8dc fix(whatsapp): warn once when group inbound dropped for missing channels.whatsapp.groups entry (#83833)
Merged via squash.

Prepared head SHA: 8fc5243210
Co-authored-by: zhang-liz <13132583+zhang-liz@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-05-26 01:15:24 -03:00
Peter Steinberger
e752f9bca1 chore(release): refresh plugin sdk api baseline 2026-05-26 05:03:33 +01:00
Peter Steinberger
c43ed9e4fe test(whatsapp): stabilize media format expectations 2026-05-26 05:03:22 +01:00
Vincent Koc
1e9b6b7627 test(qqbot): make OPENCLAW_HOME media test Windows-safe 2026-05-26 05:52:05 +02:00
Vincent Koc
a9bf582684 fix(test): forward installer smoke controls 2026-05-26 05:51:05 +02:00
Peter Steinberger
21aefb877a test: align image fast path expectations 2026-05-26 04:48:20 +01:00
Peter Steinberger
c4f0682396 test: align pnpm cache workflow assertion 2026-05-26 04:41:44 +01:00
Peter Steinberger
4118a32aad test: enforce per-test ci threshold 2026-05-26 04:35:20 +01:00
Ayaan Zaidi
4fdf61753a ci(mantis): pass crabbox capacity regions 2026-05-26 09:02:11 +05:30
Peter Steinberger
bc3d6bafae ci: disable pnpm action cache on Windows 2026-05-26 04:31:33 +01:00
Gio Della-Libera
17ab9b967c fix(agents): skip wildcard catalog metadata refs (#86524)
* fix(agents): skip wildcard catalog metadata refs

* fix(models): skip wildcard configured rows
2026-05-25 20:22:32 -07:00
Vincent Koc
947febb2fb fix(test): bootstrap macos script stdin 2026-05-26 05:17:50 +02:00
Peter Steinberger
bee8ad34a0 test(codex): avoid app-server diagnostic notification race 2026-05-26 04:17:16 +01:00
clawsweeper[bot]
7fbca96a0c fix(embedded-runner): preserve provider errors on cleanup takeover (#84321)
Summary:
- The PR preserves provider-facing embedded-runner prompt errors when cleanup detects session takeover, keeps the takeover signal fatal for fallback, and adds focused regressions.
- PR surface: Source +52, Tests +92. Total +144 across 5 files.
- Reproducibility: yes. Source inspection shows current main can let cleanup takeover replace a prior prompt/p ... rror and can normalize a provider-looking takeover wrapper before fallback sees it as coordination failure.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(embedded-runner): preserve takeover during fallback
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8405…

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

Prepared head SHA: 050c779cfa
Review: https://github.com/openclaw/openclaw/pull/84321#issuecomment-4492087335

Co-authored-by: abnershang <abner.shang@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-26 03:09:26 +00:00
Marcus Castro
bcde7b138a fix(agents): handle preflight compaction no-op budgets (#86709)
* fix(auto-reply): accept no-op preflight compaction

* fix(agents): clamp compaction runtime budget

* fix(agents): clamp queued compaction budget
2026-05-26 00:02:18 -03:00
Sliverp
0d23c3b4e1 fix: make QQ Bot media paths respect OPENCLAW_HOME configuration (#85309)
* fix: make QQ Bot media paths respect `OPENCLAW_HOME` configuration

* docs(changelog): note QQ Bot OPENCLAW_HOME media fix (#83562)
2026-05-26 11:01:39 +08:00
Vincent Koc
a695c28bfb fix(tooling): skip gauntlet declaration prebuild 2026-05-26 05:01:03 +02:00
clawsweeper[bot]
c9d0464ed1 fix(control-ui): support raw edits from editable config (#86726)
Summary:
- Merged fix(control-ui): support raw edits from editable config after ClawSweeper review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(control-ui): support raw edits from editable config

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

Prepared head SHA: befbe16362
Review: https://github.com/openclaw/openclaw/pull/86726#issuecomment-4539541885

Co-authored-by: BlackFrameAI <122847831+BlackFrameAI@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-26 02:58:20 +00:00
Omar Shahine
5a33378f9c revert: iMessage group media attachment command (#86734)
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-25 19:47:16 -07:00
Peter Steinberger
609d70d35e fix(release): stabilize beta validation after rebase 2026-05-26 03:30:54 +01:00
Vincent Koc
4738d0a296 fix(test): measure kitchen sink gateway children 2026-05-26 04:29:42 +02:00
Marcus Castro
34d862d45d fix(whatsapp): restore ack emoji identity fallback (#86697) 2026-05-25 23:25:00 -03:00
Omar Shahine
f32273257c fix(imessage): send group media via attachment command
* fix(imessage): send chat media via attachment command

* fix(imessage): satisfy bundled lint

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-25 19:24:37 -07:00
Vincent Koc
eab8d29db2 fix(test): harden plugin gauntlet proof 2026-05-26 04:23:04 +02:00
Peter Steinberger
93015982d3 fix(release): stabilize beta validation after main rebase 2026-05-26 03:06:26 +01:00
Peter Steinberger
6f57286678 refactor: use Rastermill for image processing (#86621)
* refactor: use Rastermill for image processing

* docs: clarify autoreview heartbeat patience

* refactor: use simplified rastermill api

* fix: preserve rastermill media safety boundaries

* build: update rastermill api pin

* build: use published rastermill package
2026-05-26 02:54:49 +01:00
Peter Steinberger
0c5f622f9a perf(discord): use libopus-wasm for voice opus 2026-05-26 02:53:29 +01:00
clawsweeper[bot]
3d0659433e fix(build): pin synthetic auth runtime dist entry (#86714)
Summary:
- Adds `plugins/synthetic-auth.runtime` as an explicit tsdown dist entry and adds a regression test tying PI model-discovery synthetic-auth imports to that stable entry.
- PR surface: Tests +22, Other +1. Total +23 across 2 files.
- Reproducibility: yes. as a source-reproducible package-build path: current main imports synthetic-auth from  ... y. The PR proof covers emitted production `dist/` imports, though it did not run a live scheduled cron job.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(build): pin synthetic auth runtime dist entry

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

Prepared head SHA: cb99947919
Review: https://github.com/openclaw/openclaw/pull/86714#issuecomment-4538919657

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-26 01:51:50 +00:00
clawsweeper[bot]
fddca995e8 fix(plugin-sdk): preserve string-const unions as flat enum for deepseek tool schemas (#86712)
Summary:
- This PR changes DeepSeek provider tool-schema normalization to convert multi-value string const unions into flat string enums, with regression coverage for pure, nullable, and single-const union cases.
- PR surface: Source +27, Tests +84. Total +111 across 2 files.
- Reproducibility: yes. source-level reproduction is high confidence: current main selects only the first non-null anyOf/oneOf variant, and the linked source PR proof shows before/after output for that exact schema shape.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(plugin-sdk): preserve string-const unions as flat enum for deepse…

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

Prepared head SHA: 310d95e327
Review: https://github.com/openclaw/openclaw/pull/86712#issuecomment-4538892244

Co-authored-by: 1052326311 <1052326311@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-26 01:43:06 +00:00
Vincent Koc
2e6ba44706 fix(perf): bound session transcript stat fanout 2026-05-26 03:39:45 +02:00
Vincent Koc
6984a823af fix(test): bound plugin gauntlet prebuilds 2026-05-26 03:38:09 +02:00
Peter Steinberger
743bce2c27 perf: speed up usage cost lookups 2026-05-26 02:28:30 +01:00
Alex Knight
f824e1596a Add OpenTelemetry LLM content spans (#86191)
* feat: add otel llm content spans

* fix: gate otel tool definitions separately

* fix(diagnostics): sanitize tool_call parts and truncate oversized OTEL content attributes

* fix: keep otel content truncation parseable

* fix: simplify codex model diagnostics

* fix(diagnostics): align opt-in GenAI span shape

* test(codex): align resume params after rebase

* fix(diagnostics): keep model content off shared event bus

* test(diagnostics): keep extension tests on sdk boundary

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-26 02:24:02 +01:00
Peter Steinberger
592f192bf0 chore: remove unused tracked assets 2026-05-26 02:21:58 +01:00
Peter Steinberger
010a79b5d8 fix(ui): refresh raw copy i18n baseline 2026-05-26 02:17:54 +01:00
Peter Steinberger
8f1f7901b9 fix(release): accept optional Discord voice decoder 2026-05-26 02:17:54 +01:00
Peter Steinberger
c410658725 fix(perf): tolerate passing filtered release gates 2026-05-26 02:17:54 +01:00
Peter Steinberger
e049105891 fix(release): stabilize beta validation tests 2026-05-26 02:17:54 +01:00
Vincent Koc
f2142ebf3a fix(packaging): bound dist inventory filesystem scans 2026-05-26 03:16:41 +02:00
Vincent Koc
669df88249 fix(test): remove image tool timeout slack 2026-05-26 03:04:54 +02:00
Fermin Quant
c9364f03dc fix(cron): accept opaque session target keys
Fixes #64030.

Allows cron `session:` targets to carry opaque session-store keys, including slash and backslash characters, while keeping cron job IDs on the stricter UUID/non-path contract. Adds regression coverage across cron normalization, cron service persistence, gateway cron validation, and related session target handling.

Thanks @ferminquant for the fix.

Verification:
- `git diff --check origin/main...HEAD`
- `OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs src/cron/session-target.test.ts src/cron/normalize.test.ts src/cron/service.jobs.test.ts src/cron/service/store.test.ts src/gateway/server-cron.test.ts src/gateway/server.cron.test.ts src/cron/run-log.test.ts src/gateway/protocol/cron-validators.test.ts src/agents/tools/message-tool.test.ts src/agents/tools/image-tool.custom-provider-auth.regression.test.ts --reporter dot` passed: 13 files, 347 tests.
- GitHub `checks-node-agentic-agents` reran green on `51949741a333363586ddfb4445b82116c3bcea43`.

Co-authored-by: Fermin Quant <ferminquant@hotmail.com>
2026-05-26 01:39:04 +01:00
Patrick Erichsen
24d58af560 fix: honor skill source install aliases (#84842) 2026-05-25 17:37:35 -07:00
Vincent Koc
6421808c27 fix(test): avoid message tool bundled channel loads 2026-05-26 02:29:28 +02:00
Vincent Koc
80aa6d77fc test(onboard): guard docker e2e resources 2026-05-26 02:26:28 +02:00
Peter Steinberger
d00d0a21c2 chore: bump OpenClaw to 2026.5.26 2026-05-26 01:26:00 +01:00
Peter Steinberger
321f06ad0e fix: stabilize discord voice receive recovery 2026-05-26 01:22:21 +01:00
Peter Steinberger
ee51169b20 perf: reduce session and auth cache hotpath work (#86678)
Move immutable session-store snapshot cloning/freezing off the write path and rebuild snapshots lazily on read. Resolve runtime external auth profiles once per auth-profile save instead of once per OAuth profile.

Proof: oxfmt targeted files; pnpm tsgo:core; pnpm check:test-types; node scripts/run-vitest.mjs src/config/sessions.cache.test.ts src/agents/auth-profiles.store.save.test.ts src/agents/auth-profiles/external-oauth.test.ts; autoreview clean.
2026-05-26 01:19:52 +01:00
Vincent Koc
9e93431ae9 fix(qa): stream gateway gauntlet prebuild output 2026-05-26 02:18:48 +02:00
brokemac79
56633e4f3c fix(cli): route plugin packaging recovery hints
Route invalid-config recovery output for source-only installed plugin packages to plugin packaging guidance instead of openclaw doctor --fix.

Validated with focused config/CLI/gateway/plugin tests, autoreview, Crabbox/Testbox E2E tbx_01ksgr80tnvvc13kv6t126yv78, and green PR CI on 3b3ce73d0f.

Thanks @brokemac79.
2026-05-26 01:13:20 +01:00
Deepflame
ea2496b00c perf(agents): reuse model manifest context
Reuse a lazy model manifest context across configured model resolution so common static defaults do not trigger manifest metadata loads, while keeping plugin-owned normalization available when aliases, provider rows, or OpenRouter compat paths need it.

Preserves exact alias behavior, auth-profile-suffixed alias behavior, provider inference from manifest-normalized configured refs, and existing plugin/runtime cache lifecycle rules.

Co-authored-by: Alyana <alyana@lumina.local>
2026-05-26 01:11:47 +01:00
Vincent Koc
ef8619d5f5 fix(diagnostics): expose missing telemetry signals (#86682) 2026-05-26 01:10:59 +01:00
Peter Steinberger
71e9eaab14 perf: avoid extra session snapshot cloning 2026-05-26 01:08:47 +01:00
Peter Steinberger
c59635ae97 fix: avoid compaction checkpoint transcript copies (#86666) 2026-05-26 00:59:20 +01:00
Peter Steinberger
6814525867 fix: preserve code mode failure output 2026-05-26 00:54:00 +01:00
Peter Steinberger
1514cc84cb test: avoid message tool discovery in send helper 2026-05-26 00:53:37 +01:00
Vincent Koc
6defcb0a40 fix(scripts): bound guard inventory file reads 2026-05-26 01:49:15 +02:00
Vincent Koc
60afca187d fix(test): isolate kitchen sink rpc home env 2026-05-26 01:46:51 +02:00
Kevin Lin
719ce7f96f feat(signal): support reaction approvals (#85894)
* feat(signal): support reaction approvals

* fix(signal): harden approval reaction bindings

* fix(signal): quiet native approval prompt flow

* test(prompts): refresh direct channel snapshots

* fix(signal): suppress duplicate exec approval prompts

* revert(reply): keep direct inbound metadata

* docs: add signal approval changelog

* test(prompts): restore direct channel snapshots

* fix(signal): allow defaultTo approval reactions
2026-05-25 16:44:12 -07:00
Vincent Koc
57748a66fd fix(scripts): bound source scan file reads 2026-05-26 01:43:43 +02:00
Vincent Koc
2a6b4ed3e2 test(ollama): support cloud api live smoke 2026-05-26 01:43:03 +02:00
Peter Steinberger
978a2d01da test: serialize agents tools vitest files 2026-05-26 00:42:46 +01:00
tanshanshan
3a4f2b17fc fix(auto-reply): use context-aware overflow reserve hints (#84399)
Use the effective runtime/model context when computing overflow recovery reserveTokensFloor hints, including uncataloged runtime refs, stale session windows, and heartbeat fallback cases.

Verification:
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts
- autoreview clean on final focused fixup; prior accepted findings addressed before push.
- CI passed on head e25b3e84f4 after rerunning cancelled jobs: preflight, critical quality network-runtime-boundary, security high, checks, Real behavior proof.

Co-authored-by: tanshanshan <tanshanshan@users.noreply.github.com>
2026-05-26 00:33:56 +01:00
Lellansin Huang
6c7b3f3f23 feat(gateway): forward OpenAI sampling params (#84094)
Forward OpenAI-compatible frequency_penalty, presence_penalty, and seed params through the gateway/chat-completions path while keeping Responses untouched.

Verification:
- pnpm test src/gateway/openai-http.test.ts src/agents/pi-embedded-runner/extra-params.sampling.test.ts src/agents/openai-transport-stream.test.ts
- CI passed on head 9abb9466d9 after rerunning cancelled jobs: preflight, critical quality network-runtime-boundary, security high, checks, docs, Real behavior proof.

Co-authored-by: lellansin <lellansin@gmail.com>
2026-05-26 00:33:26 +01:00
Peter Steinberger
068924e2d4 perf: cache model cost indexes
Cache configured model cost indexes for repeated session usage cost lookups while preserving in-place config mutation behavior via value-fingerprint invalidation. Raw pricing lookups now skip manifest model-id normalization as well as runtime/plugin normalization, keeping direct cost lookup off plugin metadata hot paths.

Verification:
- node scripts/run-vitest.mjs src/utils/usage-format.test.ts
- pnpm exec oxfmt --check src/utils/usage-format.ts src/utils/usage-format.test.ts
- pnpm lint --threads=8
- pnpm tsgo:core
- autoreview --mode local
- PR CI green on head 15c1e25d95
2026-05-26 00:29:55 +01:00
Peter Steinberger
5dc704361f fix: hide unsupported best effort message option 2026-05-26 00:27:57 +01:00
Peter Steinberger
bef0ba8f5a refactor: reuse realtime output activity in google meet (#86665) 2026-05-26 00:19:35 +01:00
Vincent Koc
84929e4265 fix(test): harden bundled plugin install sweep 2026-05-26 01:17:12 +02:00
Peter Steinberger
c87957db5e fix: prefer source public artifacts in source checkouts 2026-05-26 00:17:04 +01:00
Peter Steinberger
65a210553b test: type child process spawn mock 2026-05-26 00:11:40 +01:00
Vincent Koc
fe3374789f test(installer): cover rocky cli installs 2026-05-26 01:07:39 +02:00
Peter Steinberger
da831e2b8a docs: update changelog for landed fixes 2026-05-26 00:04:56 +01:00
Fermin Quant
399c692895 fix: dampen repeated device-required probes 2026-05-26 00:04:37 +01:00
Bryan Tegomoh
fc2d2d595c fix(ui): keep local file markdown links inert 2026-05-26 00:04:32 +01:00
Fermin Quant
342bde2af6 fix(update): avoid duplicate plugin smoke failures 2026-05-26 00:04:27 +01:00
Galin Iliev
d7361eff66 fix(gateway): cap retained compaction checkpoint bytes
Cap retained compaction checkpoint snapshots by total bytes per session while preserving the existing count cap.

The gateway now stats retained checkpoint snapshots inside the session-store writer before trimming, deletes older trimmed checkpoint files, and keeps the newest checkpoint available. Regression coverage uses real sparse checkpoint files to prove byte-budget cleanup.

Closes #84822.
2026-05-25 16:04:04 -07:00
Peter Steinberger
c1a026a976 fix: stabilize tests and reduce plugin memory churn 2026-05-26 00:01:30 +01:00
Peter Steinberger
1d21224de3 perf: reduce runtime metadata hotpath churn
Reduce runtime metadata hotpath churn by freezing loaded plugin metadata snapshots once and returning the memoized object without clone-on-hit. Reuse persisted package file signatures while preserving realpath containment, cache normalized Jiti alias maps by identity, and defer Discord realtime turn retention/logging until audio starts.

Verification:
- node scripts/run-vitest.mjs src/talk/turn-context-tracker.test.ts src/plugins/plugin-metadata-snapshot.memo.test.ts src/plugins/manifest-registry-installed.test.ts src/plugins/sdk-alias.test.ts src/plugins/installed-plugin-index-records.test.ts
- node scripts/run-vitest.mjs src/plugins/plugin-metadata-snapshot.memo.test.ts
- pnpm test extensions/discord/src/voice/manager.e2e.test.ts --testNamePattern "keeps realtime playback alive|interrupts realtime playback|does not interrupt realtime provider state"
- pnpm lint --threads=8
- pnpm exec oxfmt --check src/plugins/plugin-metadata-snapshot.ts src/plugins/plugin-metadata-snapshot.memo.test.ts src/plugins/manifest-registry-installed.ts src/plugins/installed-plugin-index-record-builder.ts src/plugins/sdk-alias.ts extensions/discord/src/voice/realtime.ts
- pnpm tsgo:core
- pnpm tsgo:extensions
- pnpm build
- autoreview --mode commit --commit HEAD
- PR CI green on head 7dd3e44a78
2026-05-25 23:59:45 +01:00
Peter Steinberger
a4f12699cf refactor: share realtime output activity tracking (#86661) 2026-05-25 23:51:34 +01:00
Peter Steinberger
acbdb8c373 fix(memory-wiki): bound compile page reads (#86660)
Summary
- Bound Memory Wiki compile-time page summary reads through the existing concurrency helper.
- Preserve deterministic result ordering before title sort and keep the helper in stop-on-error mode.
- Replaces #84458 because the fork branch does not allow maintainer edits and the contributor changelog entry needed removal.

Behavior addressed: Memory Wiki compile no longer starts one page-summary read per page without a bound.
Real environment tested: Local macOS source checkout, Node/pnpm repo environment.
Exact steps or command run after this patch: pnpm test extensions/memory-wiki/src/compile.test.ts; pnpm exec oxfmt --check --threads=1 extensions/memory-wiki/src/compile.ts extensions/memory-wiki/src/compile.test.ts; .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --no-web-search --prompt "Review PR #84458 after maintainer fixup. Focus on memory-wiki compile page summary read concurrency, runTasksWithConcurrency result/error handling, ordering preservation, and test reliability."
Evidence after fix: compile.test.ts passed 10 tests; oxfmt reported clean; autoreview reported no accepted/actionable findings.
Observed result after fix: Page reads are executed through runTasksWithConcurrency with errorMode stop, successful results are consumed in input-index order, and the existing summary title sort remains deterministic.
What was not tested: Full repository suite.

Co-authored-by: zhengzuo0-ai <zheng.zuo0@gmail.com>
2026-05-25 23:49:43 +01:00
Vincent Koc
00f9809531 test(qa-matrix): use larger media coverage jpeg 2026-05-25 23:45:04 +01:00
YBoy
bec7d56b73 fix(cli): reject unknown command help roots (#81083) (thanks @YB0y)
Behavior addressed: Unknown CLI command roots now error consistently even when --help or --version is appended, while legitimate built-in help fast paths still render normally.

Real environment tested: Local OpenClaw source checkout plus GitHub workflow run-level status.

Exact steps or command run after this patch: pnpm test src/cli/run-main.exit.test.ts src/cli/argv.test.ts src/cli/argv-invocation.test.ts; pnpm exec oxfmt --check --threads=1 src/cli/run-main.ts src/cli/run-main.exit.test.ts; autoreview --mode branch --base origin/main --no-web-search.

Evidence after fix: Focused CLI test shards passed 178 tests; formatter clean; autoreview reported no accepted/actionable findings; GitHub CI run 26422344121 and CodeQL Critical Quality run 26422344090 completed successfully.

Observed result after fix: `openclaw foo --help` and `openclaw foo --version` reject before proxy/program startup, while known help fast paths remain ahead of the unknown-root guard.

What was not tested: Full local build; contributor PR body already supplied build/CLI command proof before rebase.

Co-authored-by: YB0y <brianandez6@gmail.com>
2026-05-25 23:38:43 +01:00
Peter Steinberger
68ab48b179 test: improve test profiling helpers 2026-05-25 23:36:34 +01:00
Peter Steinberger
ec7ad3b4ac perf: reduce fuzzy matching allocations 2026-05-25 23:36:07 +01:00
Peter Steinberger
1531fe2525 perf: reduce runtime cache churn 2026-05-25 23:35:06 +01:00
Peter Steinberger
0164fd5e99 refactor: reuse forced consult coordinator in discord voice (#86656) 2026-05-25 23:34:17 +01:00
Iftekhar Uddin
5e8a9a905d fix(scripts): drain codex-cli metadata stdout (#84239) (thanks @IftekharUddin)
Behavior addressed: The codex-cli metadata branch no longer calls process.exit(0) immediately after writing stdout, and it still emits exactly one unsupported-backend JSON object.

Real environment tested: Local OpenClaw source checkout on macOS with Node/tsx.

Exact steps or command run after this patch: pnpm test test/scripts/print-cli-backend-live-metadata.test.ts test/scripts/docker-build-helper.test.ts; node --import tsx scripts/print-cli-backend-live-metadata.ts codex-cli | python3 -c 'import sys,json; print(json.load(sys.stdin)["provider"])'; autoreview --mode branch --base origin/main --no-web-search.

Evidence after fix: Focused tooling test shard passed 2 files / 23 tests; direct pipe parse printed codex-cli; autoreview reported no accepted/actionable findings; PR status rollup was clean.

Observed result after fix: stdout is parseable as a single JSON payload and the normal metadata path is skipped for codex-cli.

What was not tested: Live provider metadata paths beyond the focused existing test coverage.

Co-authored-by: Iftekhar Uddin <ifuddin3@gmail.com>
2026-05-25 23:27:13 +01:00
Vincent Koc
75ac0b5ed9 fix(test): avoid discord voice tts activation tax 2026-05-26 00:19:17 +02:00
Dmitry Golubev
0f35ec29d3 fix(codex): disable native thread personality (#85891) (thanks @lastguru-net)
Behavior addressed: Native Codex app-server threads now disable Codex's built-in personality on thread/start, thread/resume, turn/start, bound conversation turns, and /btw side-thread forks so OpenClaw agent workspace identity stays authoritative.

Real environment tested: Local OpenClaw source checkout plus GitHub CI on PR #85891.

Exact steps or command run after this patch: pnpm test extensions/codex/src/app-server/thread-lifecycle.test.ts extensions/codex/src/app-server/side-question.test.ts extensions/codex/src/conversation-binding.test.ts extensions/codex/src/app-server/schema-normalization-runtime-contract.test.ts; pnpm check:docs; pnpm prompt:snapshots:check; OPENCLAW_ADDITIONAL_BOUNDARY_SHARD=1/4 OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY=4 node scripts/run-additional-boundary-checks.mjs.

Evidence after fix: Focused Codex test shard passed 4 files / 79 tests; docs check passed; prompt snapshots are current; CI passed all code/quality checks, with only Real behavior proof failing as unrelated proof-bot gating for this non-channel change.

Observed result after fix: App-server request snapshots and unit tests include personality: "none" on native Codex start/resume/turn/fork paths.

What was not tested: A live Codex app-server model run was not executed.

Co-authored-by: Beru <beru@lastguru.lv>
2026-05-25 23:15:03 +01:00
Peter Steinberger
fda0141a01 Refactor realtime voice turn context tracking (#86650)
* refactor: share realtime turn context tracking

* chore: track realtime voice sdk api baseline

* fix: preserve pruned realtime turn handle state
2026-05-25 23:13:27 +01:00
UB
48adcb162c test(discord): cover deliver-lambda abort-skip path via processDiscordMessage integration 2026-05-25 23:11:54 +01:00
UB
3a48366f3e fix(discord): surface silent reply-delivery skips and remove runtime.error optional-chain 2026-05-25 23:11:54 +01:00
Peter Steinberger
75c6cf2966 docs: update changelog for landed bug fixes 2026-05-25 23:08:05 +01:00
Vincent Koc
0f54221f86 test(qa-matrix): use valid media coverage jpeg 2026-05-25 23:07:11 +01:00
Sebastien Tardif
0a38932ed9 fix(gmail-watcher): strip listeners from old process after settleProcess to prevent late-exit respawn 2026-05-25 23:07:06 +01:00
Sebastien Tardif
94968c83c6 fix(gmail-watcher): prevent TDZ in settleProcess and guard exit handler against stale child respawn 2026-05-25 23:07:06 +01:00
Sebastien Tardif
2ffd7a7172 fix(hooks): stop existing Gmail watcher before re-entry to prevent leaks
renewInterval is not cleared on re-entry to startGmailWatcher,
leaking the previous timer. Each config reload adds another
interval that fires independently.

Clear existing watcher state before starting a new one.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 23:07:06 +01:00
Earl Co
7b30291cc4 fix(codex): honor yolo app-server approval policy 2026-05-25 23:06:54 +01:00
Peter Steinberger
116c600f60 fix: treat zero-rate usage cost as unknown 2026-05-25 23:06:44 +01:00
Michael Zelbel
9c79a0f8f4 fix(usage-cost): invalidate durable cache on missing-cost semantics change
Bump USAGE_COST_CACHE_VERSION 3->4 so a warm .usage-cost-cache.json written by a
pre-change build is rebuilt instead of serving stale complete-$0 totals after
upgrade (the new missing-cost branch otherwise only runs when a file is rescanned).
Add a regression test asserting an older-version cache is treated as stale for an
unpriced session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:06:44 +01:00
Michael Zelbel
16702496c6 fix(usage-cost): only flag catalog-default zeros, preserve operator-configured $0
Address review: distinguish unknown pricing from an intentional free price. A
turn's all-zero cost is treated as unknown (counted toward missingCostEntries)
only when the operator did NOT explicitly configure the model's price under
models.providers -- i.e. the zero is a generated-catalog default (codex/gpt-5.x),
not a deliberate $0. Operator-configured zero-cost models keep reporting a
complete $0.

Adds resolveConfiguredModelCost() to read config-only pricing, and regression
tests for both paths (unconfigured unknown -> missing; configured free -> $0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:06:44 +01:00
Michael Zelbel
6e85869161 fix(usage-cost): preserve transport-recorded positive cost for unpriced models
Only treat an unpriced (all-zero) model's turn as missing when it has no
trustworthy recorded cost (recorded cost is 0 or absent). A turn carrying a
real positive recorded cost is preserved, fixing a regression where priced
fixtures without explicit pricing config lost their recorded cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:06:44 +01:00
Michael Zelbel
1cc0a96df1 fix(usage-cost): surface unpriced-model spend as missingCostEntries instead of $0
Models that ship an all-zero cost block (e.g. codex gpt-5.5, whose Codex
backend exposes no per-token price) made usage-cost report totalCost: 0 with
missingCostEntries: 0 -- a confident, complete $0 -- so every budget/spike
safeguard keyed off totalCost was silently blind to real pay-per-token spend.

scanTranscriptFile now treats a resolved cost config with no positive per-token
rate (and no tiered pricing) as "pricing unknown": for turns that burned tokens
it drops the transport's fabricated $0 and surfaces the turn as a missing-cost
entry, mirroring the existing tiered-pricing override. Models with positive or
tiered pricing and zero-token entries are unaffected.

Verified on a real OpenClaw 2026.5.20 host (default openai/gpt-5.5, api_key):
1,780,235 tokens that previously reported missingCostEntries 0 now report 32.

Related: #85858

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:06:44 +01:00
Peter Steinberger
c4c80cea35 test(irc): cover transient channel join 2026-05-25 23:06:35 +01:00
Peter Steinberger
9cb1e4799c fix(irc): normalize channel route ids 2026-05-25 23:06:35 +01:00
Kaili
63dee51dfb fix(irc): store inbound channel routes as channel:#name and join before send 2026-05-25 23:06:35 +01:00
Vincent Koc
cd96542d37 fix(test): harden macos onboarding e2e 2026-05-26 00:02:41 +02:00
clawsweeper[bot]
55c9a6beea fix(agents): strip markdown code spans from IDENTITY.md values and labels (#86647)
Summary:
- The PR updates `src/agents/identity-file.ts` to normalize backtick-wrapped IDENTITY.md labels and values, and adds parser/merge regression tests in `src/agents/identity-file.test.ts`.
- PR surface: Source +8, Tests +28. Total +36 across 2 files.
- Reproducibility: yes. source-reproducible with high confidence: current main strips `*` and `_` but not back ... e unnormalized string. I did not run tests because this review was required to keep the checkout read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): strip markdown code spans from IDENTITY.md values and la…

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

Prepared head SHA: 30c43defd6
Review: https://github.com/openclaw/openclaw/pull/86647#issuecomment-4537456646

Co-authored-by: nayrosk <105997554+nayrosk@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 22:00:36 +00:00
Vincent Koc
9be760fb37 test(qa): add collector-backed otel smoke 2026-05-25 23:51:17 +02:00
clawsweeper[bot]
99d96c1ff2 fix(memory-core): use CJK-aware tokenizer for dreaming dedupe (#80613) (#86645)
Summary:
- The PR extracts the CJK-aware memory tokenizer into a shared helper, routes dreaming dedupe through it, preserves MMR re-exports, and adds regression coverage for CJK and empty-token cases.
- PR surface: Source +15, Tests +96. Total +111 across 5 files.
- Reproducibility: yes. Current main has an ASCII-only tokenizeSnippet path in dreaming dedupe, and the source ... ction source bytes for the CJK failure modes; I did not run tests locally because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory-core): use Array.toSorted for #80613 lint fix
- PR branch already contained follow-up commit before automerge: fix(memory-core): preserve dedupe identity when both snippets tokeniz…
- PR branch already contained follow-up commit before automerge: fix(memory-core): rename __testing to testing in CJK regression tests…
- PR branch already contained follow-up commit before automerge: fix(memory-core): use CJK-aware tokenizer for dreaming dedupe (#80613)

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

Prepared head SHA: ca9c02734c
Review: https://github.com/openclaw/openclaw/pull/86645#issuecomment-4537414471

Co-authored-by: MoerAI <friendnt@g.skku.edu>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-25 21:50:55 +00:00
Peter Steinberger
3b0805414e ci: report memory metrics 2026-05-25 22:49:23 +01:00
Peter Steinberger
5b6d03e3e2 perf: reduce runtime cache churn
Reduce hot-path cache churn by reusing the active plugin metadata snapshot for manifest model-id normalization when safe, and by avoiding repeated JSON reparses for cached session stores while preserving clone semantics.

Verification:
- pnpm exec oxfmt --check src/plugins/manifest-model-id-normalization.ts src/plugins/manifest-model-id-normalization.test.ts src/config/sessions/store-cache.ts src/config/sessions.cache.test.ts
- node scripts/run-vitest.mjs src/config/sessions.cache.test.ts src/plugins/manifest-model-id-normalization.test.ts src/gateway/session-utils.subagent.test.ts
- pnpm tsgo:core
- autoreview clean
- PR CI green
2026-05-25 22:40:46 +01:00
Zee Zheng
0d4575a241 fix(pi-runner): flush blocks after compaction retry (#85288) (thanks @spacegeologist)
Behavior addressed: Embedded PI compaction retry now drains block replies again after the retry wait resolves, so retry-generated replies are not left behind while preserving aggregate-timeout fallback behavior.
Real environment tested: local OpenClaw focused Pi runner test shard plus contributor local live-output proof in the PR body.
Exact steps or command run after this patch: pnpm test src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts src/agents/pi-embedded-runner/run/compaction-retry-aggregate-timeout.test.ts; .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
Evidence after fix: 2 test files passed, 55 tests passed; final autoreview clean with no accepted/actionable findings.
Observed result after fix: the runner flushes before the compaction wait, waits for compaction retry, then performs a second idempotent flush when the wait resolves without timing out.
What was not tested: fresh external-channel live retry by this agent; PR retains contributor live-output proof for the delayed channel adapter path.

Thanks @spacegeologist.

Co-authored-by: zhengzuo0-ai <zheng.zuo0@gmail.com>
2026-05-25 22:27:29 +01:00
Vincent Koc
a122d804dd fix(gateway): abort stale agent runs on restart 2026-05-25 23:26:10 +02:00
Vincent Koc
4424dafe64 fix(ui): harden control e2e browser setup 2026-05-25 23:19:55 +02:00
Neerav Makwana
0f67dfd074 fix(telegram): keep overlapping DM replies deliverable (#85361) (thanks @neeravmakwana)
Behavior addressed: Telegram direct-message turns no longer drop an earlier overlapping normal reply, while authorized aborts and explicit/native/plugin/skill command turns still supersede active reply work.
Real environment tested: local OpenClaw focused Telegram test shard plus existing contributor Telegram screenshot/log proof in the PR body.
Exact steps or command run after this patch: pnpm test extensions/telegram/src/telegram-reply-fence.test.ts extensions/telegram/src/bot-message-dispatch.test.ts; .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
Evidence after fix: 2 test files passed, 93 tests passed; final autoreview clean with no accepted/actionable findings.
Observed result after fix: overlapping normal Telegram DMs use non-interrupting reply fences and both final replies remain deliverable; direct /stop, authorized built-in commands, and explicit text/native command turns still supersede.
What was not tested: fresh live Telegram Desktop rerun by this agent; PR retains contributor screenshot/log proof and the Real behavior proof bot remains red despite proof labels.

Thanks @neeravmakwana.

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
2026-05-25 22:17:39 +01:00
Merlin
f4cfa012e1 fix(openai): route compaction through Codex auth provider (#86408)
* fix(openai): route compaction through codex auth provider

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* fix(openai): honor default responses compaction threshold

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* fix(openai): preserve codex runtime routing

* docs(changelog): note Codex routing fix

---------

Co-authored-by: Merlin <258679497+funmerlin@users.noreply.github.com>
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 22:13:08 +01:00
Peter Steinberger
5dccba7405 refactor: share realtime forced consult coordination 2026-05-25 22:02:19 +01:00
Vincent Koc
f6a49a4e8a test(qa-lab): add runtime confidence reports 2026-05-25 22:00:21 +01:00
Peter Steinberger
cda7c30150 build: refresh dependency pins (#86628)
* build: refresh dependencies

* build: align pi fallback version
2026-05-25 21:55:46 +01:00
Peter Steinberger
9f7485e182 test: port release validation stabilizers 2026-05-25 21:50:49 +01:00
Bryan P
c51fa0d127 fix(cron): stop forcing message tool for delivery
Keep isolated cron announce delivery owned by runner fallback while leaving agent-initiated message sends optional. `delivery.mode: none` no longer forces message delivery, announce delivery skips fallback only after a verified same-target message-tool send, and prompt allowlist checks now match runtime tool policy normalization/group expansion.

Verified with focused cron tests, `check:changed`, autoreview, and PR CI on 7ab77bad97.

Thanks @bryanpearson.

Co-authored-by: bryanpearson <bryanmpearson@gmail.com>
2026-05-25 21:46:51 +01:00
Neerav Makwana
148db14736 fix(google): omit request config with cached content
Fix Gemini cached-content GenerateContent payloads so cached requests no longer resend request-level systemInstruction, tools, or toolConfig.

Covers explicit cachedContent and managed cacheRetention prompt caching; fixes #84919.

Proof: Real behavior proof passed on PR head 198a42bbc6 after live Gemini repro/fix evidence was added to the PR body. Focused tests and check:changed were already green.

Thanks @neeravmakwana.
2026-05-25 21:42:21 +01:00
Peter Steinberger
5a9673ecd7 test: stabilize release validation test harnesses 2026-05-25 21:35:29 +01:00
Vincent Koc
f1197ed6fc fix(test): bound kitchen sink command output 2026-05-25 22:28:56 +02:00
Peter Steinberger
4e9dac5e00 fix(discord): stabilize realtime wake-name feedback 2026-05-25 21:24:06 +01:00
Galin Iliev
b30f8e5290 test(config): guard legacy agentRuntime regression
Adds regression coverage for agents.defaults.agentRuntime schema acceptance and invalid-config doctor fix reachability.

The runtime behavior fix already landed on main in 5b9be2cdb1c01a2896783c52f5f0654c5f22a249; this PR locks the expected behavior with focused tests.

Closes #72872
2026-05-25 13:23:59 -07:00
Peter Steinberger
2afb8198c1 perf: precompute audio resample kernels
Precompute FIR resample kernels for common voice sample-rate conversions to avoid per-sample trigonometry while preserving output for tested ratios.\n\nVerification: node scripts/run-vitest.mjs extensions/voice-call/src/telephony-audio.test.ts; pnpm tsgo:core; autoreview --mode commit --commit HEAD; PR CI green.
2026-05-25 21:22:09 +01:00
Peter Steinberger
009b18c1f4 fix(codex): allow env api-key app-server bootstrap 2026-05-25 21:21:01 +01:00
Peter Steinberger
77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00
Peter Steinberger
a98660eebd fix(cron): preserve runtime snapshot for isolated delivery
Fix isolated cron delivery so agent-default derivation keeps using the paired runtime config snapshot, preserving resolved channel credentials such as Discord SecretRefs. Fixes #86545.
2026-05-25 21:10:14 +01:00
Vincent Koc
c55bee5ec7 fix(test): model active assistant failover attempts 2026-05-25 22:03:03 +02:00
Peter Steinberger
fe14bcecee docs: update changelog for bug sweep landings 2026-05-25 21:00:05 +01:00
Peter Steinberger
aa05c5c9dd test: fix mock signatures for tsgo 2026-05-25 20:57:08 +01:00
Sebastien Tardif
e7c7ee4385 docs(manifest): note safe-regex validation for modelPatterns 2026-05-25 20:57:04 +01:00
Sebastien Tardif
36f269d60b docs: document fail-closed behavior for rejected modelPatterns
Add inline comment explaining that compileSafeRegex rejects patterns
with nested repetition (ReDoS risk) and returns null. Rejected patterns
are silently skipped; the plugin will not match via that pattern but
other patterns and prefixes still apply.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 20:57:04 +01:00
Sebastien Tardif
117e08240b fix(security): guard plugin modelPatterns with compileSafeRegex
Replace raw `new RegExp(patternSource, "u")` in
`resolveModelSupportMatchKind` with the existing
`compileSafeRegex()` guard from `src/security/safe-regex.ts`.

A malicious or careless plugin manifest pattern like `(a+)+$`
causes catastrophic backtracking (ReDoS) against non-matching model
IDs. `compileSafeRegex` detects nested repetition and returns null,
which the caller now treats as a non-match (equivalent to the
previous catch-continue for invalid regex).

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 20:57:04 +01:00
Sebastien Tardif
9a6c16130a style: use bracket notation for __openclaw to satisfy no-underscore-dangle
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 20:57:01 +01:00
Sebastien Tardif
aff8e644fc test: tighten oversized metadata assertion to check exact id in __openclaw
Replace string containment check with direct field assertions:
- oversized.role is 'assistant'
- __openclaw.id is 'oversized-child' (exact match)
- parentId extraction proven by record inclusion in active tree

5/5 oversized transcript tests pass.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 20:57:01 +01:00
Sebastien Tardif
fe8d99d421 fix(security): escape field names in transcript regex extraction
extractJsonStringFieldPrefix and extractJsonNullableStringFieldPrefix
interpolate the `field` parameter into `new RegExp(...)` without
escaping.  All current callers pass hardcoded strings ("id",
"parentId", "type", "role"), but the function signature accepts
any string.  A future caller passing a field containing regex
metacharacters (e.g. "foo.bar") would match unintended patterns.

Wrap the interpolation with escapeRegExp() from src/shared/regexp.ts
so metacharacters are treated literally.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 20:57:01 +01:00
Peter Steinberger
78a1e7dfe6 fix(logging): keep string failure codes on EPIPE 2026-05-25 20:56:56 +01:00
Peter Steinberger
623a60a2b7 fix(logging): preserve failure exit on EPIPE 2026-05-25 20:56:56 +01:00
Pavel Zakharov
2aa5f1771f fix(logging): exit on stdout/stderr EPIPE instead of spinning
When the gateway process is orphaned after a systemd service restart,
the parent's journal pipe closes and every write to stdout/stderr returns
EPIPE. The previous handler swallowed it with a bare return, so background
loops (config file watcher, etc.) kept firing and the process spun at
100% CPU indefinitely.

Exit cleanly with code 0 instead — a process whose own output streams
are broken has nowhere to log and no reason to keep running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:56:56 +01:00
Peter Steinberger
778fa8705c fix(docs): keep spellcheck bash 3.2-compatible 2026-05-25 20:56:52 +01:00
Vitalii
fef57f99ba fix(scripts): docs-spellcheck.sh fails on bash 3.2 with set -u
scripts/docs-spellcheck.sh uses set -u and constructs args=( ... "${write_flag[@]}" ), where write_flag may be an empty array. On bash 3.2 (still the default /bin/bash on macOS), referencing an empty array under set -u raises an unbound variable error. Newer bash (>= 4.4) handles this expression correctly, which is why the script ships green on Linux CI runners.

Switch to the bash 3.2-safe parameter expansion ${write_flag[@]+"${write_flag[@]}"}: it expands to nothing when the array is empty and to the array contents otherwise, preserving --write behavior unchanged.

Also fixes overrideable -> overridable in docs/reference/test.md, which the now-running spellcheck surfaces.

Repro:
  bash scripts/docs-spellcheck.sh                # was: write_flag[@]: unbound variable, exit 1
  bash scripts/docs-spellcheck.sh                # now: codespell runs to completion
2026-05-25 20:56:52 +01:00
Vincent Koc
74f3a1eee2 fix(test): assert e2e agent reply payloads 2026-05-25 21:49:16 +02:00
Peter Steinberger
c88f660258 test(gateway): pin live gateway models to pi runtime 2026-05-25 20:37:33 +01:00
Peter Steinberger
a0023fbfa0 perf: speed up local TUI startup 2026-05-25 20:30:00 +01:00
Peter Steinberger
d0ab0d9922 refactor: share realtime voice activation helpers (#86615) 2026-05-25 20:25:17 +01:00
clawsweeper[bot]
170e0aac2a fix(feishu): render native presentation buttons (#86588)
Summary:
- The PR replaces Feishu presentation/action card fallback rendering with a shared JSON 2.0 button/behaviors renderer, updates native card sanitization, and expands Feishu channel/outbound tests.
- PR surface: Source +118, Tests +223. Total +341 across 5 files.
- Reproducibility: yes. source-reproducible: current main renders Feishu presentation button blocks through ma ...  help` fallback. I did not run local tests because this review was required to keep the checkout read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(feishu): render native presentation buttons
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8601…

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

Prepared head SHA: 36d6a36323
Review: https://github.com/openclaw/openclaw/pull/86588#issuecomment-4536092569

Co-authored-by: NianJiuZst <3235467914@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 19:20:42 +00:00
Vincent Koc
423f7d22bc fix(test): narrow plugin gauntlet prebuild 2026-05-25 21:07:17 +02:00
Peter Steinberger
5b6d409248 fix: route Discord gateway metadata through proxy (#86601)
* fix: route Discord gateway metadata through proxy

* fix: keep Discord gateway proxy fetch guarded
2026-05-25 19:59:51 +01:00
Peter Steinberger
f00a912c25 fix: tighten Discord voice wake matching (#86595)
* fix: tighten Discord voice wake matching

* test: avoid wildcard model runtime normalization
2026-05-25 19:51:32 +01:00
Peter Steinberger
baab4cf045 refactor(logging): share diagnostic message lifecycle
Refactor diagnostic queued/state/processed emission into a shared helper used by dispatch and isolated cron turns.

Preserve dispatch processed-event behavior, cron queue-depth symmetry, and final cron session-id adoption while adding focused helper coverage and reviewer comments for the non-obvious invariants.
2026-05-25 19:48:45 +01:00
Vincent Koc
e844d1d6e5 fix(cron): restore suspended lanes to default concurrency 2026-05-25 20:39:37 +02:00
Dallin Romney
a61d5308b5 fix(auth): emit one-shot doctor-pointer warning for Keychain-only legacy Codex OAuth profiles (#86220) 2026-05-25 11:39:32 -07:00
Peter Steinberger
9b9d8970b0 fix(codex): recover stale preflight bindings (#86602)
Fixes #86211.
Recreates the focused recovery from #86216 with maintainer-side tests.

Co-authored-by: Paul Frederiksen <paul@paulfrederiksen.com>
2026-05-25 19:37:49 +01:00
Peter Steinberger
8351556059 test(cron): pin sequential duration regression 2026-05-25 19:23:47 +01:00
Peter Steinberger
bdc6b32828 docs: update changelog for cron preservation (#86415) 2026-05-25 19:23:47 +01:00
IWhatsskill
985bc934a1 fix(cron): canonicalize preserved row ids 2026-05-25 19:23:47 +01:00
IWhatsskill
c916906584 fix(cron): preserve unsupported payload rows on writes 2026-05-25 19:23:47 +01:00
Peter Steinberger
9330b76a51 build: bump qs to patched release
Fixes Dependabot alert #118 for GHSA-q8mj-m7cp-5q26 by updating the workspace qs override from 6.14.2 to 6.15.2 and regenerating root and plugin shrinkwrap files.

Runtime surface: transitive qs consumers through Express, Slack, Feishu, Teams, ACP, and MCP paths.
2026-05-25 19:23:30 +01:00
brokemac79
1e188bcda9 fix(status): prefer active OAuth for runtime aliases
Prefer the active Claude CLI OAuth auth label when the configured Anthropic model resolves through an equivalent Claude CLI runtime alias, so `/status` no longer reports an unused env API-key label.

Also adds regression coverage for both text and message status renderers, plus the maintainer changelog entry.

Closes #80184.

Co-authored-by: brokemac79 <martin_cleary@yahoo.co.uk>
2026-05-25 19:19:51 +01:00
Vincent Koc
407cf8e328 chore(acpx): bump bundled acpx to 0.10.0 2026-05-25 19:17:25 +01:00
Peter Steinberger
c0f2d89c20 docs: make changelog release-owned 2026-05-25 19:15:37 +01:00
Sebastien Tardif
915c820c38 fix(google): stop appending preview to flash lite
Normalize Google Gemini 3.1 Flash Lite routing to the GA model id and keep the retired preview spelling as a compatibility alias. Align default alias docs, FAQ guidance, and deprecated-model manifest recommendations with the GA id.

Fixes #86151.

Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 19:12:36 +01:00
Peter Steinberger
cd7994f227 docs: update changelog for bug sweep landings 2026-05-25 19:06:08 +01:00
Vincent Koc
44bb0be033 fix(crabbox): detect timed macos js commands 2026-05-25 20:05:26 +02:00
Ayaan Zaidi
cf275676f3 fix(mantis): release telegram user leases on startup failure 2026-05-25 23:34:35 +05:30
Peter Steinberger
baf469f02e fix(agents): notify stale cron media failures 2026-05-25 19:04:03 +01:00
Peter Steinberger
f01b2a8eab fix(agents): deliver stale cron media completions 2026-05-25 19:04:03 +01:00
ai-hpc
f5d2db2a60 fix(agents): keep cron media completions run-scoped 2026-05-25 19:04:03 +01:00
tianxiaochannel-oss88
9445960d9d guide workspace-only scratch paths 2026-05-25 19:03:57 +01:00
Peter Steinberger
207a5a2983 fix(cron): report rotated session in final diagnostics 2026-05-25 19:03:50 +01:00
Arnab Saha
48532227d5 fix(cron): gate lifecycle diagnostic events behind isDiagnosticsEnabled
Address clawsweeper P2: cron isolated-agent lifecycle (message.queued,
session.state, message.processed) now mirrors the dispatch path and
respects the diagnostics.enabled master toggle. Added regression test
for the disabled-config path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:03:50 +01:00
Arnab Saha
804a31ec5c fix(cron): address review — drop unsupported taskLabel, pair with session.state lifecycle, add regression test + changelog 2026-05-25 19:03:50 +01:00
Arnab Saha
6ccd4e72f0 fix(cron): emit message.queued/processed for isolated-agent turns 2026-05-25 19:03:50 +01:00
haoxingjun
b5ada806dd fix: hydrate current turn image attachments 2026-05-25 19:03:43 +01:00
YUHAO-corn
177ebdc24c fix(gateway): ignore inherited launchd env for respawn 2026-05-25 19:03:37 +01:00
Vincent Koc
b0c8a4d11d fix(test): preserve undici exports in discord proxy tests 2026-05-25 20:02:10 +02:00
Peter Steinberger
bc12e04993 fix: raise default cron concurrency 2026-05-25 18:59:26 +01:00
liaoyl830
6e8d2dbbbc fix(doctor): skip restart prompt when gateway is healthy after recent restart (#86533)
* fix(doctor): skip restart prompt when gateway is healthy after recent restart

`openclaw doctor` unconditionally prompted "Restart gateway service now?"
with default=Yes whenever the gateway was running, even if it had just
restarted via SIGUSR1 after an update. This caused restart loops on macOS
where the prompt raced with launchctl KeepAlive.

Changes:
- Probe gateway health before the restart prompt when a restart handoff
  exists (deep doctor mode). If healthy, skip the prompt entirely.
- Change `initialValue` from `true` to `false` as a safety net so users
  don't accidentally confirm a restart by pressing Enter.
- Update existing test that expected a single `readGatewayRestartHandoffSync`
  call (now called twice: diagnostic display + health-probe check).

Fixes #86518

* fix(doctor): correct GatewayRestartHandoff mock types in tests

Add explicit literal types + satisfies constraint so the mock handoff
objects match the exact GatewayRestartHandoff type expected by the
type-check CI.

* fix(doctor): apply recent-restart skip to normal doctor flow

* test(doctor): align normal-flow handoff expectation

* chore: add doctor restart prompt changelog

---------

Co-authored-by: OpenClaw Contributor <openclaw-contributor@example.com>
Co-authored-by: liaoyl830 <267396060+liaoyl830@users.noreply.github.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-05-25 13:53:28 -04:00
brokemac79
8129dba5d8 fix: emit agent.send lifecycle hooks on rotation (#85875)
* fix: emit agent send lifecycle hooks

* fix(gateway): align agent send session lifecycle hooks

* fix(gateway): emit agent lifecycle before validation exits

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 18:51:13 +01:00
Vincent Koc
7cd15d2493 fix(crabbox): bootstrap macos shell js commands 2026-05-25 19:39:28 +02:00
Peter Steinberger
822ee62947 fix: tighten openshell exec preflight 2026-05-25 18:36:55 +01:00
brokemac79
aafed830a5 fix: preflight malformed openshell exec commands 2026-05-25 18:36:55 +01:00
Peter Steinberger
f87aa0ff1b docs: clarify unshipped compat policy 2026-05-25 18:35:25 +01:00
NVIDIAN
8061d66713 fix(update): allow package-manager hardlinks in swaps
Allow package-manager-managed hardlinked package roots during update/install swaps while keeping generic plugin, hook, and dependency-free install moves fail-closed.

Fixes #85559.

Co-authored-by: ai-hpc <mail.speedy.hpc@hotmail.com>
2026-05-25 18:30:49 +01:00
Peter Steinberger
17954a4f33 docs: ban repo-hosted proof artifacts 2026-05-25 18:25:57 +01:00
Josh Avant
c5b987274a fix(discord): restore bare numeric channel sends (#86571)
* fix(discord): restore bare numeric channel sends

* docs: add Discord channel send changelog
2026-05-25 10:24:20 -07:00
Vincent Koc
b83dfcb953 fix(installer): handle alpine apk runtime floors 2026-05-25 19:23:10 +02:00
Sally O'Malley
bd65b4232a fix(security): audit Claude permission overrides under YOLO (#86557)
* fix(agents): warn on Claude permission overrides under YOLO

* fix: narrow Claude audit backend guard

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 18:18:51 +01:00
Peter Steinberger
5ae91f01fa fix: speed up Discord voice wake consults 2026-05-25 18:09:16 +01:00
Vincent Koc
3eb06e305e fix(qa): harden restart inflight Windows scenario 2026-05-25 18:49:04 +02:00
Jason (Json)
5cfa577778 Recover Codex context overflow prompt errors (#85542)
* fix: recover codex context overflow prompt errors

* test: align Codex overflow prompt proof

* test: satisfy manifest registry mock contract

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 17:44:48 +01:00
Peter Steinberger
d967760b41 docs: update changelog for #70473 2026-05-25 17:35:47 +01:00
FullerStackDev
d5b0174eb1 fix(agents): derive overflow budgets from provider errors 2026-05-25 17:35:47 +01:00
Peter Steinberger
313762282c fix(plugins): only memoize complete metadata snapshots 2026-05-25 17:29:46 +01:00
Peter Steinberger
a11d4e6871 docs: update changelog for media wake fallback (#85489) 2026-05-25 17:23:28 +01:00
Jason (Json)
1b64ccbfff fix: fallback after active media wake failure (#85489)
* fix: fallback after active media wake failure

* docs: clarify generated media fallback docs
2026-05-25 17:23:20 +01:00
Peter Steinberger
159e4406ab perf(plugins): reuse derived metadata snapshots 2026-05-25 17:20:39 +01:00
Jason (Json)
f271f003d4 docs: require maintainer-editable PR branches
Require contributor and agent-created PR branches to stay maintainer-editable, with a GitHub Actions/secrets caveat for fork PRs.

Verification:
- pnpm docs:list
- git diff --check
- Real behavior proof: https://github.com/openclaw/openclaw/actions/runs/26409882732/job/77741796262
- check-docs: https://github.com/openclaw/openclaw/actions/runs/26409857961/job/77741751070

Changelog intentionally skipped per maintainer request.

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-05-25 17:19:40 +01:00
Peter Steinberger
dd375f9fc3 docs: note agent transcript OpenClaw session scan 2026-05-25 16:48:42 +01:00
Peter Steinberger
4012ae4f42 fix: scan OpenClaw sessions in agent transcript finder 2026-05-25 16:48:42 +01:00
Peter Steinberger
fc93af5637 docs: require generic local fixes 2026-05-25 16:45:56 +01:00
Peter Steinberger
a9c91ca81f fix: broaden leading voice wake fuzzing 2026-05-25 16:45:56 +01:00
Gio Della-Libera
657b246e56 test(agents): preserve provider hook mock exports (#86523) 2026-05-25 08:45:37 -07:00
Gio Della-Libera
fbb6340542 Policy: add agent-scoped policy overlays (#85817)
* feat(policy): add agent-scoped policy overlays

* docs(policy): use generic agent-scoped examples

* fix(policy): generalize scoped policy overlays

* fix(policy): clean scoped overlay checks

* fix(policy): evaluate inherited scoped agent posture

* chore(policy): keep agent harness out of scoped policy pr
2026-05-25 08:45:16 -07:00
Sebastien Tardif
abe99230df fix(kilocode): normalize string stop param to array in stream wrapper (#86461)
* fix(kilocode): normalize string stop param to array in stream wrapper

* fix: move kilocode stop normalization into extension

* fix: keep kilocode stream wrapper plugin-local

* fix: normalize kilocode stop after extra body

* fix(qa-lab): preserve WhatsApp RTT source literal

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 16:40:07 +01:00
Gio Della-Libera
dc17412c3a Doctor: expose shell completion health findings (#85566)
* feat(doctor): expose shell completion health findings

* fix(doctor): keep shell completion lint informational
2026-05-25 08:39:41 -07:00
Sally O'Malley
f0b6f70053 fix(agents): honor effective exec policy for Claude live Bash (#86330)
* fix(agents): answer Claude live control_request can_use_tool via exec policy

Claude CLI emits stream-json control_request frames with subtype
can_use_tool when it wants to use a native tool. The Claude live-session
bridge previously dropped these frames, leaving Claude waiting for a
control_response until the 180/600s no-output timeout fired (see #80819).

Resolve the effective OpenClaw exec policy (per-agent tools.exec -> global
tools.exec -> allowlist/on-miss defaults) once at session-start time and
thread it through fingerprinting and the session record. When a
can_use_tool request arrives:

- Allow native Bash when the resolved policy is security=full, ask=off
  (matching the bypassPermissions semantics OpenClaw already documents).
- Otherwise deny with a message that names the resolved policy and
  points the agent at OpenClaw MCP tools.

Unsupported control_request subtypes get a structured error response
instead of a silent no-op, and stray control_response frames are
silently dropped. Adds spawn-test coverage for both allow and deny paths.

Fixes #80819

* fix(agents): align Claude live control_request policy with backend defaults

Resolve the effective exec policy through the same defaults that
extensions/anthropic/cli-shared.ts:isOpenClawRequestedYolo and
src/agents/exec-defaults.ts:resolveExecDefaults already use (security
?? "full", ask ?? "off") instead of falling back to a hand-rolled
allowlist/on-miss default that disagreed with the rest of the codebase.
Without this, a default-config OpenClaw deployment launches Claude with
--permission-mode bypassPermissions but the bridge would still deny
Bash control_requests, re-creating the #80819 stall for the very
default-config case the issue reports.

Also thread the effective Claude permission mode into the policy
decision. Prefer the operator's explicit --permission-mode in argv,
falling back to what normalizeClaudePermissionArgs would have inserted
for an un-overridden launch. Native Bash is auto-allowed only when the
effective mode is bypassPermissions AND tools.exec resolves to
full/no-ask, so explicit raw-arg overrides like --permission-mode
default or acceptEdits broaden Claude's native prompting and are
honored by routing through deny.

Adds a no-config regression test (default deployment allows Bash, no
stall) and a permission-mode-override test (tools.exec full/off plus
explicit --permission-mode default in raw args denies). Existing
allow/deny tests continue to pass via the synthesized-mode fallback.

* fix(agents): honor effective exec policy for Claude live Bash

---------

Co-authored-by: Guillaume Thirry <g.thirry@gmail.com>
2026-05-25 11:39:17 -04:00
Vincent Koc
99997e4441 fix(test): stabilize e2e runtime imports 2026-05-25 17:35:26 +02:00
Vincent Koc
633e4b8a7c fix(test): clean plugin gauntlet temp roots 2026-05-25 17:29:51 +02:00
Peter Steinberger
69d728ac4f perf: cache plugin package realpaths (#86517) 2026-05-25 16:26:36 +01:00
Vincent Koc
2cac9e54b4 fix(qa): settle restart races with live budget 2026-05-25 17:20:54 +02:00
Vincent Koc
50d6611c10 test(crabbox): tolerate Windows shell capture 2026-05-25 17:20:54 +02:00
Vincent Koc
8a93851ee2 fix(qa): extend config cleanup Windows budget 2026-05-25 17:20:54 +02:00
Vincent Koc
e97e831c12 fix(crabbox): sync full sparse lease runs 2026-05-25 17:20:54 +02:00
Vincent Koc
3f363e0450 fix(qa): extend config mutation Windows budget 2026-05-25 17:20:54 +02:00
Chunyue Wang
89aea9b843 fix(sessions): stop doctor OOM on large session stores and reclaim stale store temps (#85967)
* fix(sessions): stop doctor OOM on large session stores and reclaim stale store temps

`openclaw doctor` loaded the full sessions.json via loadSessionStore with the
default cache-write plus return clone, materializing a multi-hundred-MB
monolithic store several times and exhausting the heap (#56827). The read-only
doctor checks (state integrity, heartbeat target, codex route scan) now load
with { skipCache: true, clone: false } so the store is materialized once.

Orphaned session-store atomic-write temps were also never reclaimed: the store
write went through the generic atomic writer, staging a shared
.fs-safe-replace.<pid>.<uuid>.tmp not identifiable as a store temp. Give the
store write a store-specific tempPrefix so its temps stage as
sessions.json.<pid>.<uuid>.tmp, classify them (isSessionStoreTempArtifactName),
and reclaim stale ones via the disk-budget sweep and the unreferenced-artifact
prune on a short staleness window so in-flight temps are preserved.

Fixes #56827

* docs(changelog): note large session store doctor fix

* test(qa): preserve WhatsApp RTT source literal

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 16:19:35 +01:00
clawsweeper[bot]
c4bce00727 fix(ollama): strip inline kimi cloud reasoning leak (#86515)
Summary:
- This PR adds an Ollama Kimi-cloud visible-content sanitizer for streamed and final assistant replies, updates stream handling and regression tests, and adds a changelog entry.
- PR surface: Source +183, Tests +473, Docs +1. Total +657 across 7 files.
- Reproducibility: yes. from source and the linked report: current main appends Ollama `message.content` direc ...  payload described in the issue would be shown. I did not run a live vendor repro in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(ollama): sanitize kimi inline reasoning in stream events
- PR branch already contained follow-up commit before automerge: fix(ollama): buffer kimi cloud stream reasoning
- PR branch already contained follow-up commit before automerge: fix(ollama): cover kimi inline boundary variants
- PR branch already contained follow-up commit before automerge: fix(ollama): preserve text start partial state
- PR branch already contained follow-up commit before automerge: fix(ollama): bound kimi stream sanitizer hold
- PR branch already contained follow-up commit before automerge: fix(ollama): keep kimi sanitizer deltas append-only

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

Prepared head SHA: b709229157
Review: https://github.com/openclaw/openclaw/pull/86515#issuecomment-4534945393

Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-25 15:16:42 +00:00
Neerav Makwana
bc10fad79c fix(discord): merge media captions into one message (#86487)
Summary:
- This PR changes the shared block reply coalescer/pipeline so compatible buffered visible text is merged into a following media payload, adds focused regression tests, and records a Discord changelog fix.
- PR surface: Source +50, Tests +175, Docs +1. Total +226 across 6 files.
- Reproducibility: yes. Current main has a clear source reproduction path: media enqueue forces a text flush and then sends the media payload separately, and the PR adds focused tests for the corrected merge path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: route streamed media through reply coalescer
- PR branch already contained follow-up commit before automerge: fix(discord): merge media captions into one message
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8648…

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

Prepared head SHA: ceafbeaf3c
Review: https://github.com/openclaw/openclaw/pull/86487#issuecomment-4534402219

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 15:16:29 +00:00
esadomer
8f260de3e7 fix(utils): clamp fetch timeout timers (#85985) 2026-05-25 16:16:09 +01:00
Anup Sharma
276ba1090e fix(ui): preserve user code block rendering (#85942) 2026-05-25 16:16:04 +01:00
BonRaynn
16ffc2507a fix(memory): prevent silent vector index degradation when embedding provider temporarily unavailable (#85704)
* fix(memory): prevent silent vector index degradation when embedding provider temporarily unavailable

Two related bugs cause complete loss of semantic vector data:

1. Promise cache deadlock in ensureProviderInitialized():
   When the embedding provider (e.g. local MLX server on port 8123) is
   temporarily unreachable at Gateway startup, loadProviderResult() throws
   and providerInitPromise becomes a permanently-cached Rejected Promise.
   The  block only clears it on success (providerInitialized=true),
   so the stale rejection blocks all future init attempts until Gateway restart.

2. Silent fts-only overwrite in runSync():
   With the provider stuck at null, shouldRunFullMemoryReindex() compares
   the stored meta.model (e.g. 'jina-embeddings-v5-text-small') against the
   runtime provider model, and since provider is null, falls through to the
   'meta.model !== fts-only' check — returning true. This triggers a full
   reindex where every file is written as fts-only, silently erasing all
   existing 11k+ semantic vectors.

Fix 1: Clear providerInitPromise in the catch block so the next call can
retry initialization (self-healing when the provider comes back online).

Fix 2: Guard runSync() — if requestedProvider is set and not 'none', but
the runtime provider is null, throw an error instead of silently degrading
to fts-only. This protects existing vector data by failing loudly.

Tested on production: 11,715 chunks + 1024-dim vectors fully preserved
after Gateway restart with the fix applied. The guard correctly blocks
sync when MLX is offline and allows normal operation when it recovers.

* fix: use this.settings.provider instead of private requestedProvider

The guard clause in runSync() was referencing this.requestedProvider
which is a private property on the MemoryIndexManager subclass and not
accessible from MemoryManagerSyncOps. Use this.settings.provider
instead, which is the same value and is accessible via the protected
abstract settings property.

* fix(memory): narrow degradation guard to only protect existing semantic indexes

The previous guard was too broad — it blocked sync for ALL non-none
provider configurations when provider was null, including the default
'auto' path where users without embedding credentials legitimately
build FTS-only indexes.

Narrow the guard to only abort when:
1. provider is null (embedding unavailable)
2. existing index metadata has a semantic model (not 'fts-only')
3. settings.provider is configured and not 'none'

This preserves the legitimate FTS-only fallback for auto/no-provider
users while still protecting existing semantic vector indexes from
silent degradation.

Reported-by: ClawSweeper (PR #85704 review)

* test: cover memory semantic index outage guard

* fix: protect semantic memory index fallback paths

* test: update memory sync harnesses

---------

Co-authored-by: Bo Yan <yaaboo-gif@users.noreply.github.com>
Co-authored-by: Yan Bo <yanbo@Mac.lan>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 16:15:59 +01:00
Peter Steinberger
8da8bc4aad docs: clarify agent transcript placeholders 2026-05-25 16:07:41 +01:00
Peter Steinberger
bb6f37e777 test(qa): annotate live transport RTT measurements 2026-05-25 15:56:13 +01:00
clawsweeper[bot]
aa702cf3db fix(qqbot): derive outbound watchdog from configured timeouts (#85267) (#86500)
Summary:
- The branch replaces QQBot's hardcoded outbound response watchdog with a resolver based on existing agent/provider `timeoutSeconds` settings, adds regression tests, and updates the changelog.
- PR surface: Source +113, Tests +116, Docs +1. Total +230 across 5 files.
- Reproducibility: yes. at source level: current main and the latest release use a hardcoded 300000 ms QQBot o ... s an 1800s provider timeout. I did not run the reporter's live QQBot/Ollama setup in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(qqbot): cover slow provider response watchdog
- PR branch already contained follow-up commit before automerge: fix(qqbot): derive outbound watchdog from configured timeouts (#85267)
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8527…

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

Prepared head SHA: 7bd829292a
Review: https://github.com/openclaw/openclaw/pull/86500#issuecomment-4534669816

Co-authored-by: SymbolStar <symbolstar@users.noreply.github.com>
Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-25 14:52:42 +00:00
Vincent Koc
6f695c1864 fix(test): clean kitchen sink rpc temp state 2026-05-25 16:47:23 +02:00
Galin Iliev
277d8fece2 fix: quiet missing daily memory reads
Closes #82928
2026-05-25 07:42:57 -07:00
Peter Steinberger
026cfb6ba1 fix: tighten empty plugin registry reuse 2026-05-25 15:42:18 +01:00
Peter Steinberger
e7ad116b9b perf: speed up agent transcript lookup 2026-05-25 15:40:35 +01:00
clawsweeper[bot]
2e3b59bc58 fix: guard QMD session stem fallback (#86482)
Summary:
- This PR changes `resolveTranscriptStemToSessionKeys` to skip empty or missing `sessionId` values during QMD slug fallback, adds regression coverage, and adds a changelog entry.
- PR surface: Source +1, Tests +17, Docs +1. Total +19 across 3 files.
- Reproducibility: yes. from source inspection: current main reaches `normalizeQmdSessionStem(entry.sessionId) ... ad-only review, but the source PR includes a direct after-fix resolver probe for the same mixed-store case.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: guard QMD session stem fallback
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8632…

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

Prepared head SHA: 81478b0ee6
Review: https://github.com/openclaw/openclaw/pull/86482#issuecomment-4534348706

Co-authored-by: abnershang <abner.shang@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Abner Shang <75654486+abnershang@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 14:35:43 +00:00
clawsweeper[bot]
489e415339 Guard OpenAI chat payload turns (#86497)
Summary:
- Adds a scoped ModelStudio/DashScope OpenAI-compatible guard for chat payloads with no non-empty user or assi ... turn, shared turn-detection helper coverage, prompt-skip handling, regression tests, and a changelog entry.
- PR surface: Source +83, Tests +298, Docs +1. Total +382 across 10 files.
- Reproducibility: yes. source-reproducible for the OpenClaw-side malformed payload shape: current main has no ... he exact qwen-long/qwen3-coder-plus provider error was not reproduced with the available DashScope account.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: make OpenAI payload guard content-aware
- PR branch already contained follow-up commit before automerge: fix: scope openai payload turn guard
- PR branch already contained follow-up commit before automerge: Guard OpenAI chat payload turns

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

Prepared head SHA: e16a3fe9f2
Review: https://github.com/openclaw/openclaw/pull/86497#issuecomment-4534668405

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-25 14:28:03 +00:00
Peter Steinberger
459e89ada8 fix(gateway): keep session tool mirrors under pressure
Reverts the diagnostic queue-pressure suppression of non-terminal session tool mirrors from PR 84846 while keeping PR 86503 recipient dedupe intact. Session-only Control UI subscribers keep receiving tool lifecycle mirrors; overlapping run and session subscribers still receive one canonical run-scoped frame. Verification: focused gateway and diagnostic tests, diff check, changed check, and autoreview all passed.
2026-05-25 15:22:52 +01:00
Peter Steinberger
0ab63e2b18 docs: route github creation through agent transcript 2026-05-25 15:21:21 +01:00
Mason Huang
f0bfb3fc33 test(tools): add unmocked image custom-provider auth regression (#85733)
Summary:
- The branch adds an unmocked image-tool custom-provider auth regression test, fixes split agents Vitest config routing, adds routing coverage, and records a changelog entry.
- PR surface: Tests +203, Docs +1, Other +8. Total +212 across 4 files.
- Reproducibility: not applicable. as a current-main failing issue: the production runtime bug was addressed by the linked predecessor, and this PR adds regression coverage plus test-routing verification for that path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(tools): polish image auth regression and fix agents vitest routing
- PR branch already contained follow-up commit before automerge: test(tools): remove proof test filename after regression rename
- PR branch already contained follow-up commit before automerge: fix(test): remove duplicate agent shard constants
- PR branch already contained follow-up commit before automerge: test(tools): add unmocked image custom-provider auth regression
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8573…

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

Prepared head SHA: cff5476aeb
Review: https://github.com/openclaw/openclaw/pull/85733#issuecomment-4525628364

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-25 14:19:04 +00:00
Peter Steinberger
a3ae5c8382 refactor(plugin-sdk): rename plain text tool-call compat wrapper 2026-05-25 15:08:01 +01:00
Peter Steinberger
23d38e4682 docs(skills): defer private release locators 2026-05-25 15:05:37 +01:00
Peter Steinberger
b9f975b64e Replace Sharp image backend with Photon (#86437)
* refactor: replace sharp image backend with photon

* refactor: remove whatsapp jimp dependency

* chore: remove stale sharp install workarounds

* test: keep image fixtures off photon

* test: use valid prompt image fixtures

* test: account for optimized PNG fixtures

* test: use valid minimax image fixtures
2026-05-25 15:04:44 +01:00
Chunyue Wang
32ddfc22f5 fix(agents): release embedded-attempt session lock on every exit path (#86427)
* fix(agents): release embedded-attempt session lock on every exit path

The embedded run controller acquires its session write lock eagerly at
creation and released it only inside the post-run cleanup block. An
exception thrown in post-prompt processing skipped that block, so the lock
leaked to the live gateway process until the watchdog reclaimed it and
later requests to the session failed with SessionWriteLockTimeoutError.

Add an idempotent dispose() to the lock controller and call it from the
run's outer finally so the eagerly-held lock is released on every exit
path. Normal/aborted/timed-out runs still hand the lock to
acquireForCleanup first, so dispose() is a no-op then (no double release).

Fixes #86014

* fix: keep session lock teardown comment lean

* docs(changelog): note embedded session lock fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 15:03:23 +01:00
Peter Steinberger
b077c3a813 fix: accept OpenClaw voice wake confusions (#86507) 2026-05-25 15:03:16 +01:00
Vincent Koc
ee5f8c7c22 fix(crabbox): bootstrap macos js toolchain 2026-05-25 15:52:57 +02:00
Peter Steinberger
d63e8d4b4f chore: add agent transcript skill 2026-05-25 14:50:16 +01:00
Peter Steinberger
89a21db627 fix(gateway): dedupe session tool fanout
Dedupe gateway tool-event fanout so connections subscribed by both run and session receive the canonical run-scoped agent event only, while session-only subscribers keep the compatibility session.tool mirror.\n\nVerification:\n- node scripts/run-vitest.mjs src/gateway/server-chat.agent-events.test.ts\n- git diff --check\n- env -u OPENCLAW_TESTBOX pnpm check:changed\n- .agents/skills/autoreview/scripts/autoreview --mode local
2026-05-25 14:40:07 +01:00
clawsweeper[bot]
d51f26850d fix: Hook ingress token unlocks password-mode gateway auth (#86453)
Summary:
- The PR expands security audit, CLI docs, and tests so `hooks.token` reuse of active Gateway token/password auth is reported while password-mode Gateway startup remains compatible.
- PR surface: Source +178, Tests +311, Docs +14. Total +503 across 14 files.
- Reproducibility: yes. from source inspection: current main forwards a bearer token as both token and passwor ... ecause this review was read-only, but the linked issue and code path make the reproduction high confidence.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cr-fmi-hook-ingress-token-unlocks-password-mode-gateway-auth): ap…
- PR branch already contained follow-up commit before automerge: fix: include trusted proxy password in hooks token reuse check
- PR branch already contained follow-up commit before automerge: fix(gateway): audit hooks password reuse without blocking startup
- PR branch already contained follow-up commit before automerge: fix: Hook ingress token unlocks password-mode gateway auth

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

Prepared head SHA: 7c796b22ec
Review: https://github.com/openclaw/openclaw/pull/86453#issuecomment-4533831028

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: jesse-merhi
2026-05-25 13:39:56 +00:00
zhang-guiping
d6b7fe8615 fix #86077: keep fallback errors candidate scoped (#86134)
Merged via squash.

Prepared head SHA: 73cc076761
Co-authored-by: zhangguiping-xydt <275915537+zhangguiping-xydt@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-25 16:32:41 +03:00
Chunyue Wang
6f76d9f246 fix(diagnostics): reclaim wedged session lanes with a stale leaked active run (#86056)
* fix(diagnostics): reclaim wedged session lanes with a stale leaked active run

A group session lane could wedge permanently (#85639): an embedded run that dies
abnormally leaves a stale ACTIVE_EMBEDDED_RUNS handle, so the diagnostic heartbeat
classifies the lane stale_session_state (recoveryEligible without allowActiveAbort)
while stuck-session recovery reads the leaked isEmbeddedPiRunActive flag and skips
with active_reply_work — a tautology that keeps the lane forever. The age-based
escape never fires because ageMs (last-activity) resets on every incoming queued
message.

Make the active-run skip a liveness check: before keeping the lane, consult the
run's real forward-progress age (lastProgressAgeMs, not refreshed by incoming
messages). If a run flagged active has made no forward progress past the resolved
diagnostics.stuckSessionAbortMs threshold (threaded through the recovery request;
falls back to a 5-minute floor) with queued work waiting, treat it as a
leaked/dead handle and reclaim it (abort + drain + force-clear) instead of
skipping. A genuinely progressing run, or one within an operator-raised
threshold, is kept.

Fixes #85639

* test(diagnostics): cover stale active run recovery

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 14:20:59 +01:00
Peter Steinberger
e761eb8f3e fix: derive plugin media trust from metadata (#86410) 2026-05-25 14:18:36 +01:00
狼哥
75c72360ad fix(media-understanding): normalize HEIC before image descriptions (#86037)
Summary:
- The PR adds HEIC/HEIF-to-JPEG normalization before media-understanding image description providers run, with regression tests and a changelog entry.
- PR surface: Source +58, Tests +82, Docs +1. Total +141 across 6 files.
- Reproducibility: yes. at source level: current main forwards HEIC buffers to `describeImage` without normali ... ody includes a red HEIC regression test before the patch. I did not execute tests in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(media-understanding): normalize HEIC before image descriptions

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

Prepared head SHA: ed34620bd7
Review: https://github.com/openclaw/openclaw/pull/86037#issuecomment-4528578874

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 13:18:18 +00:00
Peter Steinberger
8fe4f34af2 fix: accept leading fuzzy Discord voice wake names (#86484) 2026-05-25 14:01:15 +01:00
Peter Steinberger
5d018034f6 feat: promote provider tool call stream wrapper (#86489) 2026-05-25 13:55:23 +01:00
Vincent Koc
6eb46ceac8 fix(test): dedupe kitchen sink command assertions 2026-05-25 14:48:57 +02:00
Peter Steinberger
fcf0bff929 test: derive deprecated sdk usage guard (#86403) 2026-05-25 13:45:08 +01:00
Vincent Koc
ba2b820c5c fix(qa): extend memory fallback Windows budget 2026-05-25 14:43:25 +02:00
Vincent Koc
968c87d798 fix(ui): move control ui chunking helper out of runtime source 2026-05-25 14:31:38 +02:00
Peter Steinberger
5f934830d3 fix: quiet retained lost task noise (#86475) 2026-05-25 13:28:23 +01:00
Vincent Koc
dc26069a71 fix(build): keep control ui chunking out of deadcode 2026-05-25 14:24:15 +02:00
Peter Steinberger
dc2c4aab6d fix: rotate realtime voice sessions on max duration
- Rotate OpenAI Realtime voice sessions on provider max-duration events without surfacing the expected expiry as a Discord voice error.
- Add lifecycle logging for Realtime rotation/reconnect and regression coverage for max-duration reconnect.
- Allowlist the existing Control UI chunking helper for the optional Knip unused-file guard so the dependency shard stays green on the current base.
2026-05-25 13:16:48 +01:00
Vincent Koc
fc3cd4970c fix(test): stream bundled plugin sweep logs 2026-05-25 13:37:38 +02:00
Peter Steinberger
2e7e4bc966 docs: add bugfix changelog credits 2026-05-25 12:23:06 +01:00
Rohit
a6df39dd92 fix(models): show oauth marker auth status (#86378) 2026-05-25 12:21:06 +01:00
Fermin Quant
92afd8ba25 fix: seed cron task progress summaries (#86313) 2026-05-25 12:21:02 +01:00
Steven
28f169be0c fix(update): exclude prerelease tags from stable git channel (#86260) 2026-05-25 12:20:57 +01:00
xin zhuang
c637944707 fix(doctor): warn and continue when cron job store is unreadable (#86384)
Catch non-ENOENT load failures inside maybeRepairLegacyCronStore so an
unreadable ~/.openclaw/cron/jobs.json (e.g. root-owned 0600 inside
Docker) no longer aborts the rest of the doctor health checks. The
scheduler-side loadCronStore keeps its strict throw-on-read-failure
contract.

Closes #86102

Co-authored-by: 1052326311 <1052326311@users.noreply.github.com>
2026-05-25 12:20:52 +01:00
Dirk
90caa3b610 fix(gateway): clear runtime config snapshot before in-process restart (#86388)
After config.patch writes new values to openclaw.json, a subsequent
SIGUSR1 in-process restart could overwrite them with a stale snapshot.

Root cause: run-loop's onIteration hook resets lanes and task registry,
but leaves the runtimeConfigSnapshot intact. loadConfig() then returns
the old snapshot via loadPinnedRuntimeConfig() instead of re-reading disk.

Fix: clearRuntimeConfigSnapshot() in the restart iteration hook so the
next startup reads fresh config from disk.

Refs #86350
2026-05-25 12:20:47 +01:00
Vincent Koc
d270879c4b fix(scripts): restore sparse crabbox changed gates 2026-05-25 13:16:51 +02:00
Vincent Koc
0bb9b421f3 fix(build): support Windows UI builds 2026-05-25 13:06:56 +02:00
Onur Solmaz
7ff29a9e6d Fix local embedding worker safety (#85348)
Summary:
- The PR routes local GGUF memory embeddings through a bundled worker sidecar, adds structured degradation and fallback handling, updates memory tests/build output, and keeps the local config contract unchanged.
- PR surface: Source +831, Tests +503, Docs +1, Other +2. Total +1337 across 23 files.
- Reproducibility: Do we have a high-confidence way to reproduce the issue? Source and report evidence are str ... cludes native crash logs; the exact Metal teardown abort was not reproduced in this review or the PR proof.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory): keep local embedding config unchanged
- PR branch already contained follow-up commit before automerge: fix(memory): type local embedding degradation
- PR branch already contained follow-up commit before automerge: fix(memory): refresh keywords after embedding fallback
- PR branch already contained follow-up commit before automerge: fix(memory): keep worker errors internal
- PR branch already contained follow-up commit before automerge: test: satisfy memory provider lifecycle harnesses
- PR branch already contained follow-up commit before automerge: fix: harden local embedding worker fallback

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

Prepared head SHA: 1d1fe41c4e
Review: https://github.com/openclaw/openclaw/pull/85348#issuecomment-4518516047

Co-authored-by: Onur Solmaz <onur@Onurs-MacBook-Pro.local>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-25 11:03:04 +00:00
Sergiusz
70c7d6f588 fix(ui): scope chat session picker to active agent (#85965)
* fix(ui): scope chat session picker to active agent

* fix(ui): preserve picker search result bounds

* fix(ui): keep chat picker pagination scoped

* test(ui): mock session subscription sync in chat view test
2026-05-25 20:58:30 +10:00
Nimrod Gutman
9ca52ce3d9 [codex] improve iOS realtime talk mode (#86355)
Merged via squash.

Prepared head SHA: 3f5aedb265
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-25 13:27:37 +03:00
Vincent Koc
5e944691b7 fix(scripts): dedupe docker lane resources 2026-05-25 12:14:37 +02:00
Peter Steinberger
9a60fcfd3c docs: add code size guidance 2026-05-25 11:11:02 +01:00
Vincent Koc
e9b8a6ecbf fix(test): avoid source gateway import in rpc walk 2026-05-25 12:05:31 +02:00
Peter Steinberger
f950132207 docs: add bugfix changelog entries 2026-05-25 11:01:50 +01:00
Zennn
e2c174e8c8 Fix heartbeat response loop guard (#86324) (#86357) 2026-05-25 11:00:29 +01:00
Sebastien Tardif
8b42771aab fix(memory-core): filter REM dreaming candidates to light-staged entries (#86302)
* fix(memory-core): filter REM dreaming candidates to light-staged entries

REM dreaming re-ingested the full short-term recall store independently,
ignoring which entries were staged by the light sleep phase. Because the
confidence formula heavily weights accumulated averageScore (45%) and
recallStrength (25%), old high-recall entries permanently dominated
freshly staged candidates. The intended light→REM→deep pipeline was
broken: light correctly staged current material, but REM selected a
different set entirely, so lightHits never paired with remHits for deep
ranking.

Fix: in runRemDreaming(), read the phase-signals store for keys with
lightHits > 0 and filter entries to that set before passing to
previewRemDreaming(). When no light-staged keys exist (light disabled
or first run), fall back to the full entry set for backward
compatibility.

Added readLightStagedKeys() to short-term-promotion.ts as a clean
export for reading the light-staged key set from the phase signal store.

Closes #86249

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

* fix(memory-core): keep REM staging pending

* fix(memory-core): mark REM-considered staged entries

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 11:00:24 +01:00
Sebastien Tardif
5182ebcf38 fix(telegram): propagate forum topic names into agent context (#86299)
* fix(telegram): propagate forum topic names into agent context

The topic-name-cache already tracks forum topic names via
forum_topic_created/edited/closed events in bot-message-context, but
this metadata was not surfaced in two key paths:

1. The native-command handler (bot-native-commands.ts) builds the agent
   context payload with IsForum but never looked up the cached topic
   name. Now it resolves the topic name from the cache and includes
   TopicName in the context, giving agents awareness of which forum
   topic they are responding in.

2. The action runtime (action-runtime.ts) executes createForumTopic and
   editForumTopic actions but never persisted the resulting topic
   metadata back to the cache. Now both actions write the topic name
   (and optional icon metadata) to the cache after success, ensuring
   subsequent messages in those topics can resolve the name.

Closes #86024

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

* fix(telegram): scope forum topic cache updates

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 11:00:17 +01:00
Neerav Makwana
2fcd481276 fix(slack): keep downloaded files out of reply media (#86318)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 11:00:12 +01:00
mushuiyu_xydt
9239f94e5b fix(cron): accept plus durations for one-shot jobs (#86341) 2026-05-25 11:00:06 +01:00
Peter Steinberger
e7c696a5b0 fix(plugins): clear metadata memo at lifecycle boundaries 2026-05-25 10:39:42 +01:00
Peter Steinberger
4737e19058 chore(skills): normalize release skill routing 2026-05-25 10:34:45 +01:00
Peter Steinberger
033693843c docs(release): require early performance regression check 2026-05-25 10:26:47 +01:00
Vincent Koc
9afbfc1b63 fix(qa): capture Windows gateway metrics 2026-05-25 11:24:16 +02:00
Peter Steinberger
a1fe86a0ff feat(qa): add coverage scenario matching 2026-05-25 10:22:51 +01:00
Vincent Koc
4a45098a86 fix(perf): avoid duplicate docker package ui build 2026-05-25 11:16:09 +02:00
Peter Steinberger
bbc1772f4d build: enable modern TypeScript module syntax
* build: enable modern TypeScript flags

* build: drop erasable TypeScript syntax flag

* build: keep legacy class field semantics
2026-05-25 10:10:12 +01:00
Peter Steinberger
a39a2c5acb ci: include performance evidence in release validation 2026-05-25 10:09:17 +01:00
Vincent Koc
912fdfbedd fix(providers): stream ordinary tool-like prose promptly 2026-05-25 10:53:03 +02:00
Vincent Koc
82bbcf60b0 fix(perf): harden gateway restart bench exits 2026-05-25 10:43:01 +02:00
Nimrod Gutman
c791e4242b fix(gateway): gate talk secret bootstrap handoff (#85690)
Merged via squash.

Prepared head SHA: 9247cdab05
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-25 11:34:12 +03:00
Jason (Json)
35dcd42c9d fix: suppress async media incomplete-turn errors (#85933)
* fix: suppress async media incomplete-turn errors

* fix: mark async media starts as side effects

* fix: preserve async markers in codex dynamic tool progress

* fix: carry async codex tool metadata into attempts

* fix: preserve async codex metadata across snapshots

* fix: suppress async media incomplete-turn errors (#85933) (thanks @fuller-stack-dev)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 09:17:30 +01:00
Peter Steinberger
f7fcbdb53b docs: add migrate auth changelog (#85667) 2026-05-25 09:16:37 +01:00
FullerStackDev
0a98c2d626 address migrate auth review comments 2026-05-25 09:16:37 +01:00
FullerStackDev
17edec75e4 honor migrate auth opt-out in plan 2026-05-25 09:16:37 +01:00
FullerStackDev
2016a511c3 fix migrate auth opt-out precedence 2026-05-25 09:16:37 +01:00
FullerStackDev
f9a87bf312 fix ci blockers for migrate auth 2026-05-25 09:16:37 +01:00
FullerStackDev
44bb2be0b4 fix migrate supported auth imports 2026-05-25 09:16:37 +01:00
FullerStackDev
50e6cb0828 fix migrate auth lint 2026-05-25 09:16:37 +01:00
FullerStackDev
f036bac144 migrate auth credentials 2026-05-25 09:16:37 +01:00
Vincent Koc
b1b28415c2 fix(scripts): avoid duplicate install smoke ui build 2026-05-25 09:58:39 +02:00
Jason O'Neal
b552919277 fix(telegram): preserve inbound text entities (#83873) 2026-05-25 13:27:19 +05:30
Peter Steinberger
b6b275575f chore: ignore Python bytecode caches 2026-05-25 08:54:03 +01:00
Peter Steinberger
236edb267d fix: make autoreview progress visible 2026-05-25 08:50:15 +01:00
Peter Steinberger
84ab206887 test(telegram): type topic cache harness store 2026-05-25 08:47:27 +01:00
Peter Steinberger
ff1fde1bb4 test(telegram): provide topic cache store in message context harness 2026-05-25 08:47:27 +01:00
Peter Steinberger
fbb6982e6e ci(release): fix plugin prerelease extension batch invocation 2026-05-25 08:47:27 +01:00
Peter Steinberger
be8cd12c7a test(agents): complete provider runtime test mocks 2026-05-25 08:47:27 +01:00
Peter Steinberger
a289dd9863 test(agents): sync provider runtime mocks 2026-05-25 08:46:58 +01:00
Peter Steinberger
c3ab2def0a refactor: keep plain text tool-call promotion private (#86374)
Move the plain-text tool-call promotion wrapper out of the public provider stream SDK helper and into a private local-only bundled-provider runtime seam.
2026-05-25 08:43:21 +01:00
Jason (Json)
0014724428 fix(discord): suppress self-reply prompt echoes (#86238)
* fix(discord): suppress self-reply prompt echoes

* docs(changelog): note Discord self-reply fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 08:41:07 +01:00
Peter Steinberger
c44367f1e5 docs: clarify config migration policy 2026-05-25 08:20:42 +01:00
Vincent Koc
a8fc28c71a fix(perf): fail startup bench on early gateway exit 2026-05-25 09:16:09 +02:00
Jason (Json)
cd627803a0 fix: prevent plain text tool call leaks (#86222)
Prevent plain text tool call leaks from xAI/LM Studio fallback streams.

- Promotes plain-text tool-call fallback chunks into structured tool calls.
- Strips leaked internal tool syntax before user-facing/outbound text.
- Adds regression coverage across provider stream wrappers, tool payload parsing, user-facing sanitization, and outbound send validation.

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-05-25 08:15:11 +01:00
Peter Steinberger
316d97c938 fix: handle npm min-release-age in installers
Replays #84749 because the contributor fork branch became conflicted and was no longer maintainer-writable.

Co-authored-by: TeodoroRodrigo <rodrigoteodoro.90@gmail.com>
2026-05-25 08:13:47 +01:00
xin zhuang
6704d0ab27 fix(scripts): include ui:build in build-all full and ciArtifacts profiles (#86010)
* fix(scripts): include ui:build in build-all full and ciArtifacts profiles

Closes #85206.

scripts/build-all.mjs only ran ui:build via a separate `pnpm ui:build`
command. Because `pnpm build` invokes tsdown which removes `dist/`,
a backend rebuild silently deletes any previously generated
dist/control-ui assets, leaving the gateway to serve the
"Control UI assets not found" message at startup. Documentation and
startup auto-repair masked the bug at the worst possible time
(LaunchAgent readiness / remote recovery) instead of guaranteeing the
build artifact contract.

This change adds ui:build as a build-all step after
copy-export-html-templates and before write-build-info, and includes
it in the full and ciArtifacts profiles. Minimal backend dev profiles
(gatewayWatch, cliStartup) keep their existing fast-loop step lists
and do not run ui:build.

Regression coverage:
- ciArtifacts step list assertion updated to match the new ordering.
- Three new resolveBuildAllSteps assertions: ui:build is in full and
  ciArtifacts and runs after tsdown/runtime-postbuild-stamp and before
  write-build-info; ui:build is excluded from gatewayWatch/cliStartup;
  ui:build cache outputs declare dist/control-ui.

* fix(scripts): leave ui:build uncached so dist/control-ui never restores stale build IDs

ClawSweeper review on #86010 flagged that the original ui:build cache only
hashed ui/, scripts/ui.js, and scripts/lib/copy-assets.ts, but
ui/vite.config.ts also reads package.json plus git HEAD and the
OPENCLAW_CONTROL_UI_BUILD_ID/OPENCLAW_VERSION env vars to embed a build ID
into the app and service worker. A file-input cache signature cannot
exactly invalidate those metadata sources, so a warm build-all hit could
restore a previously generated dist/control-ui after tsdown clears dist
and ship stale service-worker/app cache metadata.

Leaving the step uncached keeps the contract simple: every pnpm build
re-runs Vite, which is fast for the Control UI bundle and matches the
existing behavior of every other un-cached build-all step. Backend-only
profiles (gatewayWatch, cliStartup) are still unchanged.

Tests:
- Updated the ui:build cache assertion to require step.cache to be
  undefined and explain the metadata-input reason.
- Existing presence/order/exclusion assertions for ui:build are unchanged
  and still cover the full and ciArtifacts profile contract.

* fix(scripts): keep ui build fallback pnpm-free

---------

Co-authored-by: 1052326311 <1052326311@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 08:08:52 +01:00
Vincent Koc
73189e3ecb fix(e2e): sample Windows kitchen sink gateway RSS 2026-05-25 08:59:11 +02:00
Steven
6709f4efe5 fix(cron): respect isolated target and error on missing remove id (#86234) 2026-05-25 07:50:30 +01:00
Corey Szopinski
0580f57108 fix(pi-embedded-runner): propagate trigger-derived priority to the global lane (#86100)
enqueueSession injects sessionQueuePriority into its enqueue opts so
user-facing work (trigger=user/manual → foreground) jumps ahead of
background work (trigger=cron/heartbeat/memory/overflow → background)
in the session lane.

enqueueGlobal was passing opts through unchanged, so priority resolved
to "normal" for both lanes. Since the heavy embeddedRun body
(workspace-sandbox, core-plugin-tools, bootstrap-context, bundle-tools,
system-prompt, session-resource-loader, agent-session, stream-setup)
runs inside enqueueGlobal, the global-lane queue was effectively FIFO
between user chat and cron — defeating the priority intent on the path
where it matters most.

Inject sessionQueuePriority into enqueueGlobal the same way it's
injected into enqueueSession.

Observed in production: a 3m48s user chat on a hibernation-wake
storm at 2026-05-24T04:19:09Z, where 11 overdue cron jobs + 16
overdue agent heartbeats entered the global lane simultaneously
on hibernation resume. The chat enqueued with trigger=user landed
at the back of a 27-entry FIFO queue at priority 0 instead of
preempting at priority 1 (foreground). 62 s of the 228 s wall-clock
was waiting in that queue.
2026-05-25 07:50:25 +01:00
Vincent Koc
e2bd20f0aa fix(cli): suppress self-update version warnings 2026-05-25 08:46:31 +02:00
Shakker
aa50c51902 fix: preserve webchat source reply details 2026-05-25 07:33:18 +01:00
Kevin Lin
0dabb7010b docs: replace OpenClaw docs skill and add plugin permissions guide
* docs: replace openclaw docs skill

* docs: align technical documentation skill policy

* docs: restore openclaw refactor docs skill
2026-05-24 23:20:14 -07:00
clawsweeper[bot]
b962110637 fix(codex): preserve source reply mode for active runs (#86325)
Summary:
- This PR forwards Codex app-server source reply delivery mode into active run handling, adds a focused regression test, and adds a changelog entry.
- PR surface: Source +1, Tests +38, Docs +1. Total +40 across 3 files.
- Reproducibility: yes. Source inspection shows the shared active-run queue rejects `message_tool_only` replies when the active handle lacks that mode, and current main's Codex app-server handle omits it.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): preserve source reply mode for active runs

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

Prepared head SHA: d8fac59d8f
Review: https://github.com/openclaw/openclaw/pull/86325#issuecomment-4531516197

Co-authored-by: Fermin Quant <ferminquant@hotmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 06:00:47 +00:00
Val Alexander
ab910f88ad fix: make compaction reinjection opt-in
Summary:
- Make post-compaction AGENTS.md reinjection explicit opt-in for configured sections.
- Carry the run workspace into compaction-safeguard AGENTS.md reads.
- Improve collapsed Control UI tool rows while preserving raw expanded tool details.

Verification:
- CI green on PR head 96101664f0.
- pnpm exec oxfmt --check --threads=1 <changed files>
- OPENCLAW_OXLINT_SKIP_PREPARE=1 node scripts/run-oxlint.mjs <changed ts/mjs files>
- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo
- git diff --check origin/main...HEAD && git diff --check
- node scripts/run-vitest.mjs src/agents/pi-hooks/compaction-safeguard.test.ts src/agents/pi-embedded-runner/extensions.test.ts -t "workspace"
- node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts -t "reads opted-in post-compaction context"
- node scripts/run-vitest.mjs test/scripts/test-projects.test.ts -t "allows explicit split Vitest config targets"
- node scripts/run-vitest.mjs ui/src/ui/chat/tool-cards.test.ts ui/src/ui/chat/tool-cards.node.test.ts ui/src/ui/chat/grouped-render.test.ts ui/src/styles/chat/tool-cards.test.ts
- AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode branch

Fixes #45488.
Fixes #45649.
Supersedes #67090.
2026-05-25 00:59:59 -05:00
Alex Knight
c3c8a65373 fix codex usage-limit recovery copy (#86305) 2026-05-25 15:53:40 +10:00
Val Alexander
3dd0e8ed6a feat(ui): add ephemeral Activity tab
Fixes #12831.

Adds a Control UI Activity tab at `/activity` under the Control nav group. The tab derives browser-local, memory-only activity entries from the existing `session.tool` / tool-event delivery path and stores only sanitized summaries, hidden-argument counts, and redacted/truncated output previews.

Includes filtering, tool selection, clear, expand/collapse, keyboard-native disclosure rows, auto-follow scrolling, navigation/i18n/docs/changelog coverage, and focused regression tests. Follow-up tracks #54577, #37816, and #47386 remain distinct and open.

Verification:
- `pnpm ui:i18n:sync`
- `git diff --check`
- Focused Vitest coverage for Activity, gateway/tool stream, chat item rendering, navigation, and gateway agent events
- Desktop/mobile browser smoke for sanitized Activity rendering and header de-duplication
- Testbox `pnpm check:changed`: `tbx_01ksen33c79b8rywayf6cxww4r`

Thanks @BunsDev.
2026-05-25 00:45:23 -05:00
Vincent Koc
a5d5604198 fix(tests): harden native macos plugin proof 2026-05-25 07:21:12 +02:00
clawsweeper[bot]
d3c293d9c8 fix(commitments): serialize load-modify-save with in-process queue + cross-process file lock (#86326)
Summary:
- The PR adds a commitments-store writer helper, wraps load-modify-save mutators and expiry cleanup with a per-path queue plus `withFileLock`, adds three concurrency regressions, and updates the changelog.
- PR surface: Source +153, Tests +61, Docs +1. Total +215 across 4 files.
- Reproducibility: yes. Source inspection on current main shows the unqueued load-modify-save mutation path, a ... inked proof log shows the Promise.all repro changing from 20/20 lost writes before the patch to 0/20 after.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(commitments): serialize load-modify-save with in-process queue + …

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

Prepared head SHA: a349f41ccf
Review: https://github.com/openclaw/openclaw/pull/86326#issuecomment-4531553610

Co-authored-by: ai-hpc <mail.speedy.hpc@hotmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-25 05:18:19 +00:00
pashpashpash
dd47e479ae Fail Codex compaction at the Codex boundary (#85958) 2026-05-24 22:12:34 -07:00
sallyom
908b894432 fix(docker): restore config parent ownership 2026-05-25 01:00:10 -04:00
Tak Hoffman
3a03dd5712 docs: clarify config default review policy (#86329) 2026-05-24 23:58:28 -05:00
Shakker
0eead19fec docs: clean changelog script entries 2026-05-25 05:54:06 +01:00
Vincent Koc
5bd5509e06 fix(scripts): budget restart benchmark timeouts 2026-05-25 06:38:43 +02:00
Shakker
730fd1907f fix: align ui vitest config assertion 2026-05-25 05:17:45 +01:00
Shakker
56a383cdfa fix: route explicit ui vitest targets narrowly 2026-05-25 05:17:45 +01:00
Shakker
777402eeb5 fix: route unit ui vitest targets narrowly 2026-05-25 05:17:45 +01:00
Ayaan Zaidi
deb54b5dab fix(android): harden play media permission removal 2026-05-25 09:43:15 +05:30
Val Alexander
119a01c829 fix(webchat): stabilize live transcript run state
Stabilize WebChat transcript/run-state truth for Codex and selected-session observers.

Summary:
- Mirror Codex inbound prompts at turn start without duplicating suppressed persisted prompts.
- Deliver hidden external-channel live chat/tool/agent updates only to exact selected-session subscribers.
- Repair Control UI selected-session subscription state, alias-aware run adoption, and accumulated stream dedupe.
- Add focused Codex, gateway/session-event, and Control UI regression coverage.

Verification:
- Current-head CI: 101 green, 0 pending; stale canceled entries are superseded automation from prior force-pushed heads.
- Local focused Vitest shards passed: Codex app-server 2 files / 233 tests, gateway/session 4 files / 116 tests, UI 7 files / 238 tests.
- `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo`
- `node --import tsx scripts/check-no-extension-test-core-imports.ts`
- `git diff --check origin/main..HEAD`

Closes #83528.
Closes #82611.
Refs #83949.
2026-05-24 23:07:29 -05:00
Vincent Koc
95d1b39b96 fix(scripts): fail restart benchmark regressions 2026-05-25 05:41:38 +02:00
Ayaan Zaidi
9db04a27eb fix(openai): scope external codex auth to realtime 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
ffb02a5919 fix(android): stabilize realtime talk connection state 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
4656275202 fix(openai): discover codex cli auth for provider checks 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
48c4f57401 fix(openai): prefer codex auth for GPT realtime 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
70614f88cc fix(android): keep talk mode on realtime relay 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
d7aa1f31de test(android): add gateway connect adb probe 2026-05-25 09:01:07 +05:30
Ayaan Zaidi
e52a3b31e4 test(android): add voice mode adb e2e harness 2026-05-25 09:01:06 +05:30
Vincent Koc
3db1508f1e fix(ci): stabilize deadcode and catalog checks 2026-05-25 04:54:24 +02:00
Vincent Koc
ca70015a7c fix(scripts): prebuild gateway cpu bench 2026-05-25 04:24:01 +02:00
Vincent Koc
4798264a29 fix(e2e): harden bundled lifecycle probe on Windows 2026-05-25 04:14:00 +02:00
Vincent Koc
60c0f249ad test(e2e): sample kitchen sink rpc peak rss 2026-05-25 03:50:01 +02:00
Vincent Koc
ea3bb9282c fix(scripts): remove stale deadcode allowlist entries 2026-05-25 03:40:29 +02:00
Galin Iliev
b5c1199217 fix(telegram): route polling diagnostics away from errors
Route normal [telegram][diag] polling diagnostics through runtime.log while keeping non-diag Telegram warnings/errors and offset persistence failures on runtime.error.

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

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

Closes #84470.
Refs #84503.

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

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

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

* fix: clean up windows browser mcp tree before close

* fix(browser): repair chrome mcp cleanup rebase

---------

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

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

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

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

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

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

* test(agents): keep provider test mocks current

---------

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

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

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

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

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

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

---------

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

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

* docs: add telegram pairing changelog

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

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

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

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

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

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

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

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

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

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

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

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

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

* docs: add ClawSweeper review policy to AGENTS

* docs: move ClawSweeper review policy into AGENTS.md

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Addresses 15 findings from the multi-angle codex review:

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

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

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

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

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

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

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

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

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

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

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

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

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

Focused suite: 48 passed (was 47).

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

Co-authored-by: Andy Ye <andy@Andys-MacBook-Pro-2.local>
Co-authored-by: Andy Ye <andylye@outlook.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 14:56:17 +00:00
Vincent Koc
694d45e535 fix(media): use static image compression metadata 2026-05-24 16:47:59 +02:00
Peter Steinberger
01c5ab8d13 fix(release): verify large plugin npm packs 2026-05-24 15:33:17 +01:00
Vincent Koc
7e51f83aec fix(test): require kitchen sink diagnostic canaries 2026-05-24 16:27:46 +02:00
2470 changed files with 106809 additions and 40724 deletions

View File

@@ -0,0 +1,88 @@
---
name: agent-transcript
description: "Add a redacted agent transcript section to GitHub PR or issue bodies during OpenClaw agent-created PR/issue workflows."
---
# Agent Transcript
Best-effort local-only provenance for OpenClaw PR/issue bodies. Use during agent-created GitHub PR or issue workflows before creating/updating the body.
## Contract
- Never use network. Session discovery reads local agent logs only.
- Never upload raw logs. Render sanitized Markdown first.
- Always ask the user before adding transcript logs to a GitHub PR/issue body.
- Tell the user sanitized session logs help reviewers and can make PRs easier to prioritize.
- Offer a local HTML preview before insertion. If the user wants preview, open it and wait for confirmation before adding the section.
- Fail closed on unresolved secrets, private keys, browser/session/cookie details, or auth URLs.
- Drop system/developer prompts, raw tool outputs, reasoning, env, cookies, tokens, and broad local paths.
- Keep user prompts, assistant visible decisions, terse tool summaries, and test/proof outcomes.
- Remove session turns unrelated to the PR/issue work. Use the PR/issue title, branch name, changed files, and stated goal as scope; omit earlier/later unrelated tasks even when they are in the same session log.
- Best effort only: PR/issue creation must continue if no safe transcript is found.
- Add the `## Agent Transcript` section only when inserting a real transcript. Never add a placeholder transcript heading or text such as "A sanitized local transcript preview was generated but not included."
- Use a collapsed `<details>` section and update existing markers instead of duplicating sections.
## Helper
```bash
.agents/skills/agent-transcript/scripts/agent-transcript --help
```
Find a likely local session:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript find \
--query "$PR_TITLE $BRANCH_OR_PR_URL" \
--cwd "$PWD" \
--since-days 14
```
`find` scans the newest 400 matching local JSONL logs by default across Codex, Claude, Pi, and OpenClaw agent sessions. Use `--max-files N` for a wider local search.
Render a PR/issue body section:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript render \
--session "$SESSION_JSONL" \
--out /tmp/agent-transcript.md
```
Preview one candidate session locally:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript preview \
--session "$SESSION_JSONL" \
--out /tmp/agent-transcript-preview.html
open /tmp/agent-transcript-preview.html
```
Append/update a body file before `gh pr create --body-file` or connector PR creation:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript append-body \
--body /tmp/pr-body.md \
--session "$SESSION_JSONL" \
--out /tmp/pr-body.with-transcript.md
```
## PR/Issue Workflow
1. Draft the normal PR/issue body first.
2. Run `find` with title, branch, PR URL/number if known, and cwd.
3. If a high-confidence session is found, ask:
`Include a redacted agent transcript? It helps reviewers and can make the PR easier to prioritize. I can open a local preview first.`
4. If the user wants preview, run `preview`, open the HTML with `open`, and wait for confirmation.
5. Before insertion, trim unrelated session turns from the generated section. Keep only turns that explain this PR/issue's goal, implementation choices, files, tests, proof, blockers, and final outcome.
6. If the user approves, run `append-body`.
7. Use the enriched body file for creation/update.
8. If no safe session is found, say nothing and continue without transcript. If the user declines, continue without transcript and do not add any transcript placeholder section.
## Review Artifacts
For manual audits across many PR/session candidates, create a local HTML preview from a local JSON file. This is for maintainers only and is not part of the PR/issue workflow:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript html \
--prs /tmp/recent-prs.json \
--out /tmp/agent-transcript-preview.html
```

View File

@@ -0,0 +1,683 @@
#!/usr/bin/env node
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
const MARKER_START = "<!-- agent-transcript:start -->";
const MARKER_END = "<!-- agent-transcript:end -->";
const DEFAULT_MAX_CHARS = 50000;
const DEFAULT_ENTRY_MAX_CHARS = 6000;
function usage() {
console.log(`Usage:
agent-transcript find --query TEXT [--cwd PATH] [--since-days N] [--max-files N] [--root PATH...]
agent-transcript render --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N] [--title TEXT] [--url URL]
agent-transcript preview --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N] [--title TEXT] [--url URL]
agent-transcript append-body --body FILE --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N]
agent-transcript html --prs FILE [--out FILE] [--since-days N] [--min-score N] [--root PATH...] [--exclude-session FILE...]
Local-only. No network calls.`);
}
function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) {
args._.push(arg);
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (next == null || next.startsWith("--")) {
args[key] = true;
continue;
}
i++;
if (args[key] == null) args[key] = next;
else if (Array.isArray(args[key])) args[key].push(next);
else args[key] = [args[key], next];
}
return args;
}
function asArray(value) {
if (value == null) return [];
return Array.isArray(value) ? value : [value];
}
function homePath(...parts) {
return path.join(os.homedir(), ...parts);
}
function openClawSessionRoots() {
const stateDir = process.env.OPENCLAW_STATE_DIR || homePath(".openclaw");
const agentsDir = path.join(stateDir, "agents");
if (!fs.existsSync(agentsDir)) return [];
try {
const roots = fs
.readdirSync(agentsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.flatMap((entry) => {
const agentDir = path.join(agentsDir, entry.name);
return [
path.join(agentDir, "sessions"),
path.join(agentDir, "agent", "sessions"),
path.join(agentDir, "agent", "codex-home", "sessions"),
];
})
.filter((root) => fs.existsSync(root));
return [...new Set(roots)];
} catch {
return [];
}
}
function defaultRoots() {
return [
homePath(".codex", "sessions"),
homePath(".claude", "projects"),
homePath(".pi", "agent", "sessions"),
...openClawSessionRoots(),
];
}
function walkJsonl(root, sinceMs, out = []) {
if (!root || !fs.existsSync(root)) return out;
const stat = fs.statSync(root);
if (stat.isFile()) {
if (root.endsWith(".jsonl") && stat.mtimeMs >= sinceMs) out.push(root);
return out;
}
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const file = path.join(root, entry.name);
if (entry.isDirectory()) walkJsonl(file, sinceMs, out);
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
const entryStat = fs.statSync(file);
if (entryStat.mtimeMs >= sinceMs) out.push(file);
}
}
return out;
}
function readJsonl(file, maxLines = 12000) {
const text = fs.readFileSync(file, "utf8");
const lines = text.split(/\n+/).filter(Boolean).slice(0, maxLines);
const rows = [];
for (const line of lines) {
try {
rows.push(JSON.parse(line));
} catch {
rows.push({ type: "unparsed", text: line });
}
}
return rows;
}
function stringContent(value) {
if (value == null) return "";
if (typeof value === "string") return value;
if (Array.isArray(value)) return value.map(stringContent).filter(Boolean).join("\n");
if (typeof value === "object") {
if (typeof value.text === "string") return value.text;
if (typeof value.content === "string") return value.content;
if (typeof value.message === "string") return value.message;
if (Array.isArray(value.content)) return stringContent(value.content);
if (value.type === "text" && value.text) return String(value.text);
}
return "";
}
function detectAgent(file, rows) {
if (file.includes(`${path.sep}.codex${path.sep}`)) return "codex";
if (file.includes(`${path.sep}.claude${path.sep}`)) return "claude";
if (file.includes(`${path.sep}.pi${path.sep}`)) return "pi";
if (
file.includes(`${path.sep}.openclaw${path.sep}`) ||
(file.includes(`${path.sep}agents${path.sep}`) && file.includes(`${path.sep}sessions${path.sep}`))
) {
return "openclaw";
}
if (rows.some((row) => row?.type === "session_meta" || row?.type === "response_item")) return "codex";
if (rows.some((row) => row?.sessionId && row?.userType)) return "claude";
return "agent";
}
function eventText(row) {
if (row?.type === "event_msg") {
const payload = row.payload || {};
return stringContent(payload.message || payload.text_elements || payload.content);
}
if (row?.type === "response_item") {
const payload = row.payload || {};
return stringContent(payload.content || payload.summary || payload.arguments || payload.output);
}
if (row?.message) return stringContent(row.message);
if (row?.content) return stringContent(row.content);
if (row?.text) return stringContent(row.text);
return "";
}
function eventRole(row) {
if (row?.type === "event_msg") {
const type = row.payload?.type;
if (type === "user_message") return "user";
if (type === "agent_message") return "assistant";
if (type === "token_count" || type === "task_started" || type === "task_complete") return null;
if (type === "web_search_end") return "web";
}
if (row?.type === "response_item") {
const payload = row.payload || {};
if (payload.type === "function_call") return "tool";
if (payload.type === "function_call_output") return "tool_output";
if (payload.type === "reasoning") return null;
if (payload.type === "web_search_call") return "web";
if (payload.role === "user") return "user";
if (payload.role === "assistant") return "assistant";
}
if (row?.type === "user") return "user";
if (row?.type === "assistant") return "assistant";
if (row?.message?.role === "user") return "user";
if (row?.message?.role === "assistant") return "assistant";
if (row?.type === "tool_result" || row?.type === "tool_use") return "tool";
return null;
}
function hasSetupBlob(text) {
return (
text.includes("<INSTRUCTIONS>") ||
text.includes("# AGENTS.MD") ||
text.includes("Knowledge cutoff:") ||
text.includes("You are Codex") ||
/\byour instructions\b/i.test(text) ||
/\binstructions absorbed\b/i.test(text) ||
/\bAGENTS\.md\b/i.test(text)
);
}
function redact(input, stats) {
let s = String(input ?? "");
const rules = [
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]"],
[/sk-[A-Za-z0-9_-]{20,}/g, "[REDACTED_OPENAI_KEY]"],
[/(gh[pousr]_[A-Za-z0-9_]{20,})/g, "[REDACTED_GITHUB_TOKEN]"],
[/(AKIA[0-9A-Z]{16})/g, "[REDACTED_AWS_KEY]"],
[/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/g, "[REDACTED_JWT]"],
[/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi, "[REDACTED_AUTH_HEADER]"],
[/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[REDACTED_EMAIL]"],
[/\b(?:\+?\d[\d .()-]{7,}\d)\b/g, "[REDACTED_PHONE]"],
[/\/Users\/[^\s`"'>)]+/g, "[LOCAL_PATH]"],
[/~\/[^\s`"'>)]+/g, "[HOME_PATH]"],
[/([?&](?:token|key|secret|signature|sig|access_token|auth)=)[^\s`"'>&]+/gi, "$1[REDACTED]"],
];
for (const [re, repl] of rules) {
const before = s;
s = s.replace(re, repl);
if (s !== before) stats.redactions++;
}
return s;
}
function unsafe(text) {
const patterns = [
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/i,
/\b(?:user_session|_gh_sess|__Host-user_session_same_site|GH_SESSION_TOKEN)\b/i,
/\b(?:GITHUB_TOKEN|GH_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY)\b/,
/\/upload\/policies\/assets|uploadToken|authenticity_token/i,
];
return patterns.filter((pattern) => pattern.test(text)).map((pattern) => String(pattern));
}
function normalizeEntry(role, text, stats, options = {}) {
let t = redact(text, stats).replace(/\n{3,}/g, "\n\n").trim();
if (!t) return null;
if (hasSetupBlob(t)) t = "[instructions recap omitted; policy/config text, not task dialogue]";
if (unsafe(t).length) t = "[omitted: browser/session/auth internals; not useful for public PR transcript]";
const entryMaxChars = Number(options.entryMaxChars || options["entry-max-chars"] || DEFAULT_ENTRY_MAX_CHARS);
if (t.length > entryMaxChars) {
t = `${t.slice(0, entryMaxChars).trimEnd()}\n...[truncated ${t.length - entryMaxChars} chars]`;
}
return `[${role}]\n${t}`;
}
function entryRole(entry) {
const match = entry.match(/^\[([^\]]+)\]\n/);
return match ? match[1] : null;
}
function entryBody(entry) {
return entry.replace(/^\[[^\]]+\]\n/, "");
}
function coalesceEntries(entries) {
const coalesced = [];
for (const entry of entries) {
const role = entryRole(entry);
const body = entryBody(entry);
const last = coalesced[coalesced.length - 1];
if (!last || !role || entryRole(last) !== role || role === "tool summary") {
coalesced.push(entry);
continue;
}
const lastBody = entryBody(last);
if (lastBody === body || lastBody.includes(body)) continue;
if (body.includes(lastBody)) {
coalesced[coalesced.length - 1] = `[${role}]\n${body}`;
continue;
}
coalesced[coalesced.length - 1] = `[${role}]\n${lastBody}\n\n${body}`;
}
return coalesced;
}
function toolFamily(name) {
const normalized = String(name).toLowerCase();
if (
/(read|fetch|open|list|find|search|grep|rg|sed|cat|head|tail|jq|wc|status|diff|show|view|snapshot|screenshot)/.test(
normalized,
)
) {
return "read";
}
if (/(write|edit|patch|apply|create|update|append|save|comment|fill|click|type|navigate|upload)/.test(normalized)) {
return "write";
}
if (/(exec|command|shell|run|test|build|lint|format|install|pnpm|npm|node|git|gh|ssh)/.test(normalized)) {
return "execute";
}
if (/(web|http|fetch|browser|chrome|github|dropbox|notion|gmail|calendar)/.test(normalized)) {
return "network";
}
return "other";
}
function shellFamily(command) {
const cmd = String(command || "").trim();
if (!cmd) return "execute";
if (
/^(rg|grep|sed|cat|head|tail|jq|wc|ls|find|pwd|git (status|diff|show|log|blame)|gh (pr|issue|api|run|repo|auth) (view|list|status)|test |stat |ps |which |command -v )\b/.test(
cmd,
)
) {
return "read";
}
if (/^(open |chmod |mkdir |touch |cp |mv |kill |git add|git commit|git push|gh pr create|gh issue create)\b/.test(cmd)) {
return "write";
}
if (/^(node|npm|pnpm|bun|python|python3|ruby|tsx|tsgo|make|cargo|go test|swift|xcodebuild)\b/.test(cmd)) {
return "execute";
}
if (/^(ssh|curl|wget|tailscale|nc )\b/.test(cmd)) return "network";
return "execute";
}
function toolCallFamily(row) {
const name = row.payload?.name || row.name || row.message?.name || row.type || "tool";
if (name === "exec_command") {
try {
const args = JSON.parse(row.payload?.arguments || "{}");
return shellFamily(args.cmd);
} catch {
return "execute";
}
}
if (name === "apply_patch") return "write";
if (name === "write_stdin") return "execute";
return toolFamily(name);
}
function compactToolSummary(familyCounts, dropped) {
const families = new Map();
for (const [family, count] of familyCounts.entries()) {
families.set(family, (families.get(family) || 0) + count);
}
const ordered = ["read", "write", "execute", "network", "other"]
.map((family) => [family, families.get(family) || 0])
.filter(([, count]) => count > 0)
.map(([family, count]) => `${count} ${family}`);
const calls = ordered.length ? ordered.join(", ") : "0 tool";
return `${calls}; raw tool outputs dropped: ${dropped}`;
}
function recountEntries(stats, entries) {
stats.rawEntries = stats.entries;
stats.entries = entries.length;
stats.user = entries.filter((entry) => entry.startsWith("[user]\n")).length;
stats.assistant = entries.filter((entry) => entry.startsWith("[assistant]\n")).length;
}
function renderSession(file, options = {}) {
const rows = readJsonl(file);
const agent = detectAgent(file, rows);
const stats = {
agent,
entries: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolOutputsDropped: 0,
web: 0,
redactions: 0,
omittedUnsafe: 0,
};
const toolCounts = new Map();
const items = [];
const seenEntries = new Set();
const hasEventDialogue = rows.some((row) => {
const type = row?.type === "event_msg" ? row.payload?.type : null;
return type === "user_message" || type === "agent_message";
});
for (const row of rows) {
const role = eventRole(row);
if (!role) continue;
if (hasEventDialogue && row.type === "response_item" && (role === "user" || role === "assistant")) {
continue;
}
if (role === "tool_output") {
stats.toolOutputsDropped++;
continue;
}
if (role === "tool") {
const family = toolCallFamily(row);
toolCounts.set(family, (toolCounts.get(family) || 0) + 1);
stats.toolCalls++;
continue;
}
if (role === "web") {
stats.web++;
continue;
}
const before = eventText(row);
const entry = normalizeEntry(role, before, stats, options);
if (!entry) continue;
const dedupeKey = entry.replace(/\s+/g, " ").trim();
if (seenEntries.has(dedupeKey)) continue;
seenEntries.add(dedupeKey);
if (entry.includes("[omitted: browser/session/auth internals")) stats.omittedUnsafe++;
items.push(entry);
stats.entries++;
if (role === "user") stats.user++;
if (role === "assistant") stats.assistant++;
}
if (toolCounts.size) {
items.push(`[tool summary]\n${compactToolSummary(toolCounts, stats.toolOutputsDropped)}`);
stats.entries++;
}
const renderedItems = coalesceEntries(items);
recountEntries(stats, renderedItems);
const maxChars = Number(options.maxChars || DEFAULT_MAX_CHARS);
let joined = renderedItems.join("\n\n");
if (joined.length > maxChars) joined = `${joined.slice(0, maxChars).trimEnd()}\n\n...[transcript truncated to ${maxChars} chars]`;
const headerBits = [options.title, options.url].filter(Boolean).join(" | ");
const unsafeAfter = unsafe(joined);
const safe = unsafeAfter.length === 0;
const markdown = `${MARKER_START}
## Agent Transcript
<details>
<summary>Redacted ${agent} session transcript${headerBits ? `: ${redact(headerBits, stats)}` : ""}</summary>
\`\`\`\`text
source: [LOCAL_SESSION]
redaction: local paths, emails, phone-shaped strings, token-shaped strings, auth headers, auth query params
omitted: raw tool outputs, system/developer prompts, local paths, secrets, browser/session/auth details
stats: ${JSON.stringify(stats)}
${joined}
\`\`\`\`
</details>
${MARKER_END}
`;
return { file, agent, safe, unsafeAfter, stats, markdown };
}
function readBoundedText(file, maxBytes = 220000) {
const fd = fs.openSync(file, "r");
try {
const stat = fs.fstatSync(fd);
if (stat.size <= maxBytes) {
const buffer = Buffer.alloc(stat.size);
fs.readSync(fd, buffer, 0, stat.size, 0);
return buffer.toString("utf8");
}
const half = Math.floor(maxBytes / 2);
const head = Buffer.alloc(half);
const tail = Buffer.alloc(half);
fs.readSync(fd, head, 0, half, 0);
fs.readSync(fd, tail, 0, half, Math.max(0, stat.size - half));
return `${head.toString("utf8")}\n[...middle omitted for scan...]\n${tail.toString("utf8")}`;
} finally {
fs.closeSync(fd);
}
}
function sessionScanRecord(file, maxBytes) {
const stat = fs.statSync(file);
const agent = detectAgent(file, []);
return {
file,
agent,
mtime: new Date(stat.mtimeMs).toISOString(),
haystack: `${file}\n${readBoundedText(file, maxBytes)}`.toLowerCase(),
};
}
function scoreScanRecord(record, terms, cwd) {
const haystack = record.haystack;
let score = 0;
const reasons = [];
for (const term of terms) {
const normalized = term.toLowerCase().trim();
if (normalized.length < 3) continue;
if (haystack.includes(normalized)) {
score += Math.min(20, Math.max(3, Math.floor(normalized.length / 3)));
reasons.push(normalized.slice(0, 80));
}
}
if (cwd) {
const cwdLower = cwd.toLowerCase();
if (haystack.includes(cwdLower) || record.file.toLowerCase().includes(cwdLower.replaceAll("/", "-"))) {
score += 8;
reasons.push("cwd");
}
}
return { file: record.file, score, reasons, mtime: record.mtime, agent: record.agent };
}
function recentFiles(files, maxFiles) {
return files
.map((file) => {
try {
return { file, mtimeMs: fs.statSync(file).mtimeMs };
} catch {
return null;
}
})
.filter(Boolean)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, maxFiles)
.map((entry) => entry.file);
}
function candidateFiles(roots, terms, sinceMs, options = {}) {
return recentFiles(roots.flatMap((root) => walkJsonl(root, sinceMs)), Number(options["max-files"] || 400));
}
function findSessions(options) {
const sinceDays = Number(options["since-days"] || 14);
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
const roots = asArray(options.root).length ? asArray(options.root) : defaultRoots();
const query = String(options.query || "");
const terms = query
.split(/\s+/)
.concat(query.match(/https?:\/\/\S+/g) || [])
.filter(Boolean);
const files = candidateFiles(roots, terms, sinceMs, options);
const scanBytes = Number(options["scan-bytes"] || 60000);
const results = files
.map((file) => scoreScanRecord(sessionScanRecord(file, scanBytes), terms, options.cwd))
.filter((result) => result.score > 0)
.sort((a, b) => b.score - a.score || b.mtime.localeCompare(a.mtime))
.slice(0, Number(options.limit || 10));
return results;
}
function sessionScanRecords(options) {
const sinceDays = Number(options["since-days"] || 14);
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
const roots = asArray(options.root).length ? asArray(options.root) : defaultRoots();
const excluded = new Set(asArray(options["exclude-session"]).map((file) => path.resolve(file)));
return roots
.flatMap((root) => walkJsonl(root, sinceMs))
.filter((file) => !excluded.has(path.resolve(file)))
.map((file) => sessionScanRecord(file, Number(options["scan-bytes"] || 90000)));
}
function replaceSection(body, section) {
const start = body.indexOf(MARKER_START);
const end = body.indexOf(MARKER_END);
if (start !== -1 && end !== -1 && end > start) {
return `${body.slice(0, start).trimEnd()}\n\n${section.trim()}\n\n${body.slice(end + MARKER_END.length).trimStart()}`;
}
return `${body.trimEnd()}\n\n${section.trim()}\n`;
}
function escapeHtml(text) {
return String(text)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function htmlDocument(records) {
const rows = records
.map((record) => `<section>
<h2><a href="${escapeHtml(record.url || "")}">${escapeHtml(record.title || record.url || "PR")}</a></h2>
<p><code>${escapeHtml(record.session ? "[LOCAL_SESSION]" : "no session")}</code> score: ${escapeHtml(record.score ?? "")} safe: ${escapeHtml(record.safe ?? "")}</p>
<pre>${escapeHtml(record.markdown || record.error || "")}</pre>
</section>`)
.join("\n");
return `<!doctype html>
<meta charset="utf-8">
<title>Agent Transcript Preview</title>
<style>
body{font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:32px;color:#1f2328;background:#fff}
section{border-top:1px solid #d0d7de;padding:24px 0}
h1,h2{line-height:1.2}
pre{white-space:pre-wrap;background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;overflow:auto}
code{background:#f6f8fa;padding:2px 4px;border-radius:4px}
a{color:#0969da}
</style>
<h1>Agent Transcript Preview</h1>
${rows}
`;
}
function singlePreviewDocument(record) {
return htmlDocument([record]);
}
function readPrs(file) {
const raw = fs.readFileSync(file, "utf8");
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : parsed.items || parsed.prs || [];
}
function main() {
const [command, ...rest] = process.argv.slice(2);
const args = parseArgs(rest);
if (!command || command === "--help" || command === "-h" || args.help) {
usage();
return;
}
if (command === "find") {
console.log(JSON.stringify(findSessions(args), null, 2));
return;
}
if (command === "render") {
if (!args.session) throw new Error("--session is required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
if (args.out) fs.writeFileSync(args.out, rendered.markdown);
else process.stdout.write(rendered.markdown);
return;
}
if (command === "preview") {
if (!args.session) throw new Error("--session is required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
const output = singlePreviewDocument({
title: args.title || "Agent Transcript Preview",
url: args.url || "",
session: args.session,
safe: rendered.safe,
markdown: rendered.markdown,
});
if (args.out) fs.writeFileSync(args.out, output);
else process.stdout.write(output);
return;
}
if (command === "append-body") {
if (!args.body || !args.session) throw new Error("--body and --session are required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
const body = fs.readFileSync(args.body, "utf8");
const next = replaceSection(body, rendered.markdown);
if (args.out) fs.writeFileSync(args.out, next);
else process.stdout.write(next);
return;
}
if (command === "html") {
if (!args.prs) throw new Error("--prs is required");
const records = [];
const scanRecords = sessionScanRecords(args);
const minScore = Number(args["min-score"] || 50);
for (const pr of readPrs(args.prs)) {
const query = [pr.url, pr.number ? `#${pr.number}` : "", pr.number, pr.title, pr.headRefName, pr.headRefName || pr.branch]
.filter(Boolean)
.join(" ");
const terms = query
.split(/\s+/)
.concat(query.match(/https?:\/\/\S+/g) || [])
.filter(Boolean);
const [candidate] = scanRecords
.map((record) => scoreScanRecord(record, terms, args.cwd))
.filter((result) => result.score >= minScore)
.sort((a, b) => b.score - a.score || b.mtime.localeCompare(a.mtime));
if (!candidate) {
records.push({ ...pr, error: "No local session match found." });
continue;
}
try {
const rendered = renderSession(candidate.file, { ...args, title: pr.title, url: pr.url });
records.push({
...pr,
session: candidate.file,
score: candidate.score,
safe: rendered.safe,
markdown: rendered.markdown,
});
} catch (error) {
records.push({ ...pr, session: candidate.file, score: candidate.score, error: String(error) });
}
}
const output = htmlDocument(records);
if (args.out) fs.writeFileSync(args.out, output);
else process.stdout.write(output);
return;
}
usage();
process.exitCode = 2;
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}

View File

@@ -26,6 +26,9 @@ Use when:
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
- Never switch or override the requested review engine/model. If the review hits model capacity, retry the same command a few times with the same engine/model.
- Be patient with large bundles. Structured review can take up to 30 minutes while the model call is active, especially with Codex tools or web search.
- Treat heartbeat lines like `review still running: ... elapsed=... pid=...` as healthy progress, not a hang. Let the helper continue while heartbeats are advancing.
- Do not kill a review just because it has been quiet for 2-5 minutes, or because it is still running under the 30-minute window. Inspect the process only after missing multiple expected heartbeats, after 30 minutes, or after an obviously failed subprocess; prefer letting the same helper command finish.
- Tools are useful in review mode. The helper allows read-only inspection tools and web search by default so reviewers can check dependency contracts, upstream docs, and current behavior.
- Security perspective is always included, but it should not cripple legitimate functionality. Report security findings only when the change creates a concrete, actionable risk or removes an important safety check.
- Do not invoke built-in `codex review`, nested reviewers, or reviewer panels from inside the review. The helper builds one bundle, calls one selected engine, validates one structured result, and stops.
@@ -169,6 +172,7 @@ The helper:
- supports `--dry-run`, `--parallel-tests`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output
- prints `review still running: <engine> elapsed=<seconds>s pid=<pid>` to stderr at long-running intervals while waiting for the selected review engine
- prints `autoreview clean: no accepted/actionable findings reported` when the selected review command exits 0
- exits nonzero when accepted/actionable findings are present

View File

@@ -93,6 +93,37 @@ def run(args: list[str], cwd: Path, *, input_text: str | None = None, check: boo
return result
def run_with_heartbeat(
args: list[str],
cwd: Path,
*,
input_text: str | None = None,
label: str,
heartbeat_seconds: int = 60,
) -> subprocess.CompletedProcess[str]:
started = time.monotonic()
proc = subprocess.Popen(
args,
cwd=cwd,
stdin=subprocess.PIPE if input_text is not None else None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
first_communicate = True
while True:
try:
stdout, stderr = proc.communicate(
input=input_text if first_communicate else None,
timeout=heartbeat_seconds,
)
return subprocess.CompletedProcess(args, int(proc.returncode or 0), stdout, stderr)
except subprocess.TimeoutExpired:
first_communicate = False
elapsed = int(time.monotonic() - started)
print(f"review still running: {label} elapsed={elapsed}s pid={proc.pid}", file=sys.stderr, flush=True)
def git(repo: Path, *args: str, check: bool = True) -> str:
return run(["git", *args], repo, check=check).stdout
@@ -320,7 +351,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
"-",
]
)
result = run(cmd, repo, input_text=prompt, check=False)
result = run_with_heartbeat(cmd, repo, input_text=prompt, label="codex")
try:
output = output_path.read_text()
finally:
@@ -349,7 +380,7 @@ def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
cmd.extend(["--model", args.model])
if args.thinking:
cmd.extend(["--effort", args.thinking])
result = run(cmd, repo, input_text=prompt, check=False)
result = run_with_heartbeat(cmd, repo, input_text=prompt, label="claude")
if result.returncode != 0:
raise SystemExit(f"claude engine failed ({result.returncode})\n{result.stderr or result.stdout}")
return result.stdout
@@ -374,7 +405,7 @@ def run_droid(args: argparse.Namespace, repo: Path, prompt: str) -> str:
cmd.extend(["--model", args.model])
if not args.tools:
cmd.extend(["--disabled-tools", "*"])
result = run(cmd, repo, check=False)
result = run_with_heartbeat(cmd, repo, label="droid")
prompt_path.unlink(missing_ok=True)
if result.returncode != 0:
raise SystemExit(f"droid engine failed ({result.returncode})\n{result.stderr or result.stdout}")
@@ -416,7 +447,7 @@ def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
)
if args.web_search:
cmd.append("--allow-all-urls")
result = run(cmd, Path(tempdir), check=False)
result = run_with_heartbeat(cmd, Path(tempdir), label="copilot")
if result.returncode != 0:
raise SystemExit(f"copilot engine failed ({result.returncode})\n{result.stderr or result.stdout}")
return result.stdout

View File

@@ -149,7 +149,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
"pnpm test:changed"
```
Full suite:
@@ -160,7 +160,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test"
"pnpm test"
```
Focused rerun:
@@ -171,7 +171,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test <path-or-filter>"
"pnpm test <path-or-filter>"
```
Read the JSON summary. Useful fields:
@@ -206,7 +206,7 @@ node scripts/crabbox-wrapper.mjs run \
--ttl 240m \
--timing-json \
-- \
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 OPENCLAW_TESTBOX=1 OPENCLAW_TESTBOX_REMOTE_RUN=1 pnpm check:changed
corepack pnpm check:changed
```
Read the JSON summary and the Testbox line. Useful fields:
@@ -544,14 +544,14 @@ If brokered AWS cannot dispatch, sync, attach, or stop, retry once with
```sh
pnpm crabbox:run -- --debug --timing-json -- \
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed
pnpm test:changed
```
Full suite:
```sh
pnpm crabbox:run -- --debug --timing-json -- \
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test
pnpm test
```
Auth fallback, only when `blacksmith` says auth is missing:
@@ -591,7 +591,7 @@ Minimal Blacksmith-backed Crabbox run, from repo root:
```sh
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json -- \
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test:changed
corepack pnpm test:changed
```
Use direct Blacksmith only when Crabbox is the broken layer and you are
@@ -617,7 +617,7 @@ provider deliberately.
```sh
pnpm crabbox:warmup -- --class beast --market on-demand --idle-timeout 90m
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "pnpm test:changed"
pnpm crabbox:stop -- <cbx_id-or-slug>
```

View File

@@ -1,238 +0,0 @@
---
name: openclaw-docs
description: Write or review high-quality OpenClaw developer documentation.
dependencies: []
---
# OpenClaw Docs
## Overview
Use this skill when writing, editing, or reviewing OpenClaw developer documentation for APIs, SDKs, CLI tools, integrations, quickstarts, platform guides, or technical product docs.
Write documentation that is concise, helpful, and comprehensive: fast for first success, precise for production, and easy to scan when debugging.
## Core Model
Use an OpenClaw documentation model, strengthened by Write the Docs principles:
- Lead with what the developer is trying to do.
- Give one recommended path before alternatives.
- Make examples runnable and realistic.
- Keep guides task-oriented and references exhaustive.
- Explain production risks exactly where developers can make mistakes.
- Link concepts, guides, API references, SDKs, testing, and troubleshooting so readers can move between them without rereading.
- Treat docs as part of the product lifecycle: draft them before or alongside implementation, review them with code, and keep them current.
- Make each page discoverable, addressable, cumulative, complete within its stated scope, and easy to skim.
## Structure
Choose the page type before writing:
- Overview: route readers to the right product, integration path, or guide.
- Quickstart: get a new user to a working result with the fewest safe steps.
- Topic page: give an end-to-end overview of a major domain entity, with setup,
key subtopics, troubleshooting, and links to deeper references.
- Guide: explain one workflow from prerequisites to production readiness.
- API reference: define every object, endpoint, parameter, enum, response, error, and version rule.
- SDK or CLI reference: document install, auth, commands or methods, options, examples, and failure modes.
- Testing guide: show sandbox setup, fixtures, test data, simulated failures, and live-mode differences.
- Troubleshooting guide: map symptoms to checks, causes, and fixes.
Use this default topic page structure:
1. Title: name the major entity or surface.
2. Opening overview: start with a few unheaded sentences that explain what it
is, what it owns, and what it does not own. Do not add a `## Overview`
heading unless the page is itself an overview index.
3. Requirements: include only when setup needs specific accounts, versions,
permissions, plugins, operating systems, or credentials.
4. Quickstart: show the recommended setup path and smallest reliable verification.
5. Configuration: show the minimum configuration needed to use the surface,
common variants users must choose between, and where each option is set:
CLI, config file, environment variable, plugin manifest, dashboard, or API.
6. Major subtopics: organize the entity's major concepts, workflows, and
decisions by reader intent. Put each major subtopic under its own heading;
do not wrap them in a generic `## Subtopics` section.
7. Troubleshooting: diagnose common observable failures under an explicit
`## Troubleshooting` heading.
8. Related: link to guides, references, commands, concepts, and adjacent topics.
Topic pages may be longer than quickstarts, but they should not become exhaustive
references. Move field tables, API contracts, narrow internals, legacy details,
and rare debugging workflows to linked reference or troubleshooting pages when
they interrupt the end-to-end overview.
For configuration, keep task-critical options inline. Link to reference docs for
full option lists, defaults, enums, generated schemas, and advanced settings. Do
not duplicate exhaustive config reference tables in topic pages unless the topic
page is itself the reference.
Use this default guide structure:
1. Title: name the outcome, not the implementation detail.
2. Opening: state what the reader can accomplish in one or two sentences.
3. Before you begin: list accounts, keys, permissions, versions, tools, and assumptions.
4. Choose a path: compare options only when the reader must decide.
5. Steps: use verb-led headings with code, expected output, and checks.
6. Test: show the smallest reliable proof that the integration works.
7. Production readiness: cover security, idempotency, retries, limits, observability, migrations, and cleanup.
8. Troubleshooting: include common errors near the workflow that causes them.
9. See also: link to concepts, API references, SDK docs, and adjacent guides.
Keep navigation user-intent based. Do not force readers to understand internal product taxonomy before they can pick a task.
## Documentation Lifecycle
Write and maintain docs with the same discipline as code:
- Draft docs early enough to expose unclear product, API, CLI, or config design.
- Keep docs source near the code, config, command, plugin, or protocol it describes when the repo layout allows it.
- Avoid duplicate truth. If the same contract appears in multiple places, pick the canonical page and link to it.
- Update docs in the same change as behavior, config, API, CLI, plugin, or troubleshooting changes.
- Remove, redirect, or clearly mark stale docs. Incorrect docs are worse than missing docs.
- Involve the right reviewers: code owners for behavior, support or QA for user failure modes, and docs maintainers for structure and style.
- Preserve older-version guidance only when users need it; otherwise document the current supported behavior.
Do not use FAQs as a dumping ground for unrelated material. Promote recurring questions into task, concept, troubleshooting, or reference pages.
## Writing Style
Write in a direct, practical voice:
- Use present tense and active voice.
- Address the reader as "you" when giving instructions.
- Prefer short paragraphs and scannable lists.
- Use concrete nouns: "agent profile", "Gateway webhook", "plugin manifest", "session state".
- Put caveats exactly where they affect the step.
- Avoid marketing language, hype, generic benefits, and vague claims.
- Avoid long conceptual lead-ins before the first actionable step.
- Do not over-explain common developer concepts unless the product has a nonstandard contract.
- Define OpenClaw-specific jargon and abbreviations before first use.
- Use sentence case for headings unless an OpenClaw product name, command, or identifier requires capitalization.
- Use descriptive link text that names the destination or action; avoid vague links such as "this page" or "click here".
- Avoid culturally specific idioms, violent idioms, and jokes that make docs harder to translate or scan.
- Write accessible prose: do not rely on color, screenshots, or visual position as the only way to understand an instruction.
Use headings that describe actions or reference surfaces:
- Good: "Create an agent", "Configure a Slack channel", "Repair plugin installation"
- Avoid: "How it works", "Under the hood", "Important notes" unless the section truly needs that shape
Use precise modal language:
- Use "must" for required behavior.
- Use "can" for optional capability.
- Use "recommended" for the default path.
- Use "avoid" for known footguns.
- Explain "why" only when it changes a developer decision.
## Detail Level
Vary detail by page type:
- Overview pages: be brief; help readers choose.
- Quickstarts: be procedural; include only what is needed for first success.
- Guides: be complete for one workflow; include decisions, side effects, and failure handling.
- References: be exhaustive; document every field, default, enum, nullable value, constraint, response, and error.
- Troubleshooting: be explicit; assume the reader is blocked and needs observable checks.
Go deep where mistakes are expensive:
- Authentication and secret handling
- Money movement, billing, permissions, and irreversible actions
- Webhooks, retries, duplicate events, and ordering
- Idempotency and concurrency
- Sandbox versus production differences
- Versioning, migrations, and backwards compatibility
- Limits, rate limits, quotas, and timeouts
- Error codes and recovery paths
- Data retention, privacy, and compliance-sensitive behavior
Do not bury this detail in a distant reference if developers need it to complete the task safely.
## Examples
Make examples production-shaped, even when using test data:
- Prefer complete copy-pasteable commands or snippets.
- Use realistic variable names and values.
- Mark placeholders clearly with angle-bracket names such as `<API_KEY>` or `<CUSTOMER_ID>`.
- Show expected success output after commands.
- Show full request and response examples for API references when response shape matters.
- Keep one conceptual unit per code block.
- Use language-specific code fences.
- Avoid toy examples that hide required setup, auth, error handling, or cleanup.
When multiple languages are useful, keep the same scenario across languages so readers can compare equivalents.
## Discoverability and Navigation
Design every page so readers can find it, link to it, and decide quickly whether it answers their question:
- Use goal-oriented titles and headings that match likely search terms.
- Start each page with a concise answer to "what can I do here?"
- Include metadata or frontmatter required by the OpenClaw docs index.
- Add "Read when" hints for docs-list routing when creating or changing OpenClaw docs pages that participate in the docs index.
- Link from likely entry points, not only from nearby internal taxonomy pages.
- Keep section headings stable enough for links from issues, PRs, support replies, and chat answers.
- Order tutorials and examples from prerequisites to advanced tasks; order reference pages alphabetically or topically when that helps lookup.
- State scope up front when a page is intentionally partial.
## API Reference Pattern
For endpoints, methods, objects, or commands, include:
1. Short purpose statement.
2. Auth or permission requirements.
3. Request shape, including path, query, headers, and body fields.
4. Parameter table with type, requiredness, default, constraints, enum values, and side effects.
5. Return shape with object lifecycle states.
6. Error cases with codes, causes, and recovery guidance.
7. Runnable example request.
8. Representative successful response.
9. Related guides and adjacent reference pages.
For nested objects, document child fields near their parent. Do not make readers jump across pages to understand the shape of a single request.
## Verification
Verify docs changes like product changes:
- Run the relevant docs build, docs index, formatter, link checker, or generated-doc check when available.
- Run commands, snippets, and examples that the page tells users to run whenever feasible.
- Confirm screenshots, UI labels, CLI output, config keys, flags, defaults, errors, and file paths match current behavior.
- Prefer executable checks over prose-only review for API, CLI, config, generated reference, and troubleshooting docs.
- If a verification step is not feasible, say what was not verified and why.
## Completeness Checks
Before finalizing a page, verify:
- The first screen tells readers what they can accomplish.
- The recommended path is obvious.
- Prerequisites are explicit and testable.
- Examples can run with documented inputs.
- The page has a clear audience: user, operator, plugin author, contributor, or maintainer.
- Test-mode and production-mode behavior are separated.
- Security-sensitive values are never exposed in examples.
- Every warning is attached to the step where it matters.
- Edge cases are documented where they affect implementation.
- API fields include types, defaults, constraints, and errors.
- Troubleshooting starts from observable symptoms.
- Related links help the reader continue without duplicating the page.
- The page says where to get support, file issues, or contribute when that is relevant to the reader's next step.
- The page is complete for the scope it claims, or the limitation is stated up front.
## Review Pass
Edit in this order:
1. Remove repetition and generic explanation.
2. Move conceptual background below the first useful action unless it is required to choose correctly.
3. Replace passive or abstract wording with concrete instructions.
4. Tighten headings until the outline reads like a task map.
5. Add missing operational details for production safety.
6. Check examples for copy-paste accuracy.
7. Add links between guide, reference, SDK, testing, and troubleshooting surfaces.
8. Check discoverability, addressability, accessibility, and docs-as-code verification.

View File

@@ -5,7 +5,7 @@ description: Inspect, patch, validate, publish, or confirm OpenClaw GHSA securit
# OpenClaw GHSA Maintainer
Use this skill for repo security advisory workflow only. Keep general release work in `openclaw-release-maintainer`.
Use this skill for repo security advisory workflow only. Keep general release work in `release-openclaw-maintainer`.
## Respect advisory guardrails

View File

@@ -89,11 +89,11 @@ Reject:
- if unwritable or wrong shape, create own PR and preserve useful contributor credit
- if no PR exists, create one
- add regression test when it fits
- changelog for user-facing fixes; thank credited human reporter/contributor
- release-note context for user-facing fixes in PR body or commit message; credit human reporter/contributor when known
6. Review, refresh, and publish:
- rebase or otherwise refresh the PR branch on current `origin/main`
- resolve drift, including newly exposed CI failures, rather than counting the PR as ready
- changelog-only conflicts are routine on busy `main`; resolve them mechanically when already refreshing, but do not treat them as a real code conflict, a reason to reject the PR, or evidence that the branch needs extra fixup beyond the changelog entry order
- do not add `CHANGELOG.md` during normal sweep PRs; release automation generates it from PRs and commits
- left-test the rebased head with the smallest meaningful local/Testbox/live command that proves the bug
- run `$autoreview` until no accepted/actionable findings remain before creating, updating, or presenting the PR URL
- create/update PR with real body and proof fields

View File

@@ -139,12 +139,12 @@ Issue triage is review/prove/patch-local by default:
2. Fix only issues that are easy, high-confidence, and narrowly owned by the implicated path.
3. Add focused regression proof when practical.
4. Stop with the dirty diff, touched files, and test/gate output for maintainer review.
5. After maintainer approval to ship, make one commit per accepted fix, with its own changelog entry when user-facing.
5. After maintainer approval to ship, make one commit per accepted fix, with release-note context in the PR body or commit message when user-facing.
6. Pull/rebase, push, then comment and close only the issues that were fixed or explicitly triaged closed.
Do not batch unrelated issue fixes into one commit. Do not publish, comment, close, or label during the review/prove phase.
Missing changelog is not a PR review finding or merge blocker. If landing/fixing a user-visible change, add/update changelog automatically when practical; never ask or block solely on it.
Missing `CHANGELOG.md` is not a PR review finding or merge blocker. If landing/fixing a user-visible change, make sure the PR body or commit message captures the release-note context; never ask or block solely on it.
Only list candidates that pass all gates:
@@ -244,9 +244,8 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
## Follow PR review and landing hygiene
- Never mention merge conflicts that are relatively easy to resolve, such as
`CHANGELOG.md` entries, in review-only output. These are landing mechanics,
not correctness findings.
- Never mention release-note bookkeeping in review-only output. It is landing
or release-generation mechanics, not a correctness finding.
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- Before landing any PR with non-trivial code changes, run `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already covered it, the change is trivial/docs-only, or the user opts out.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "OpenClaw Plugin Pre-Release Testing"
short_description: "Plan plugin release validation"
default_prompt: "Use $openclaw-pre-release-plugin-testing to plan or run pre-release OpenClaw plugin validation across package, lifecycle, doctor, gateway, SDK, and live-ish proof."

View File

@@ -98,7 +98,7 @@ barrels, package-boundary tests, or extension suites.
- add `--keep`/`--id <id-or-slug>` only when several commands must share one
warmed box; stop it with `pnpm crabbox:stop -- <id-or-slug>`.
5. If plugin performance is package-artifact sensitive, switch to
`openclaw-pre-release-plugin-testing` and Package Acceptance rather than
`release-openclaw-plugin-testing` and Package Acceptance rather than
trusting source-only timing.
## Metric Collection

View File

@@ -1,41 +0,0 @@
---
name: optimizetests
description: Optimize OpenClaw slow tests, imports, misplaced coverage, and CI wall time without dropping coverage.
---
# Optimize Tests
Goal: real OpenClaw test/runtime speedups with coverage intact. Do not add shards,
skip assertions, weaken gates, or tune runner flags as the main fix.
## Runbook
1. Read `docs/help/testing.md`, `docs/ci.md`, and the scoped `AGENTS.md` files
for any subtree you will edit.
2. Establish evidence before edits:
- Full ranking: `pnpm test:perf:groups --full-suite --allow-failures --output .artifacts/test-perf/<name>.json`
- Targeted file: `timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose`
- Import suspicion: add `OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1`
3. Attack highest-return hotspots first:
- broad barrels or `importActual()` in hot tests
- per-test `vi.resetModules()` plus fresh imports
- expensive gateway/server/client setup where reset/reuse proves same behavior
- core tests asserting extension-owned behavior
- duplicated fixture construction or contract assertions
4. Prefer production-quality fixes:
- narrow runtime seams over broad mocks
- pure helpers for static parsing/metadata
- injected deps over module resets
- extension-owned tests for bundled plugin/provider/channel behavior
5. After each change, rerun the same benchmark and the proving test lane. Record
before/after wall time, Vitest duration, and max RSS when available.
6. Run `pnpm check:changed`; run broader gates (`pnpm check`, `pnpm test`,
`pnpm build`) when touched surfaces require them.
7. Commit scoped changes with `scripts/committer "<conventional message>" <paths...>`.
Push when requested. If CI is red, inspect with `gh run list/view`, fix, push,
repeat until current CI is green or a blocker is proven unrelated.
## Output
End with the pushed commit(s), before/after timings, gates run, current CI state,
and any remaining tail lanes that need separate optimization.

View File

@@ -1,6 +0,0 @@
interface:
display_name: "Optimize Tests"
short_description: "Benchmark and speed up OpenClaw tests"
default_prompt: "Use $optimizetests to benchmark slow OpenClaw tests, optimize imports and duplicated setup, move misplaced core coverage to extensions, verify gates, commit scoped changes, push, and keep CI green without adding shards or dropping coverage."
policy:
allow_implicit_invocation: false

View File

@@ -1,11 +1,11 @@
---
name: openclaw-release-ci
name: release-openclaw-ci
description: "Run, watch, debug, and summarize OpenClaw full release CI, release checks, live provider gates, install/update proofs, and release-secret preflights."
---
# OpenClaw Release CI
Use this with `$openclaw-release-maintainer` and `$openclaw-testing` when a release candidate needs full validation, install/update proof, live provider checks, or CI recovery.
Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a release candidate needs full validation, install/update proof, live provider checks, or CI recovery.
## Guardrails
@@ -22,7 +22,7 @@ Use this with `$openclaw-release-maintainer` and `$openclaw-testing` when a rele
Before full release validation:
```bash
node .agents/skills/openclaw-release-ci/scripts/verify-provider-secrets.mjs --required openai,anthropic,fireworks
node .agents/skills/release-openclaw-ci/scripts/verify-provider-secrets.mjs --required openai,anthropic,fireworks
gh api rate_limit --jq '.resources.core'
git status --short --branch
git rev-parse HEAD
@@ -35,6 +35,30 @@ The script prints only provider status and HTTP class, never tokens.
## Dispatch
Start product performance evidence as early as the release SHA exists, in
parallel with other release work:
```bash
gh workflow run openclaw-performance.yml \
--repo openclaw/openclaw \
--ref main \
-f target_ref=<release-sha> \
-f profile=release \
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=false
```
- Do not wait for full release validation to start this early perf signal.
- Compare available Kova, gateway startup, and CLI startup metrics with earlier
release evidence or clawgrit reports before publish/closeout.
- Call out any regression in the release proof. Treat a major regression as a
release blocker until it is fixed, waived by the operator, or proven to be
infrastructure noise.
- Full Release Validation also records advisory product-performance evidence;
the early standalone run is for overlap and faster regression discovery.
Prefer the trusted workflow on `main`, target the exact release SHA:
```bash
@@ -55,7 +79,7 @@ Use `release_profile=stable` unless the operator explicitly asks for the broad a
Use the summary helper instead of repeated raw polling:
```bash
node .agents/skills/openclaw-release-ci/scripts/release-ci-summary.mjs <full-release-run-id>
node .agents/skills/release-openclaw-ci/scripts/release-ci-summary.mjs <full-release-run-id>
```
Then watch only when useful:
@@ -85,7 +109,8 @@ Record:
- release SHA
- full parent run URL
- child run IDs and conclusions: CI, Release Checks, Plugin Prerelease, NPM Telegram
- child run IDs and conclusions: CI, Release Checks, Plugin Prerelease, NPM Telegram, Product Performance
- performance comparison result versus earlier releases when available
- targeted local proof commands
- provider-secret preflight result
- known gaps or unrelated failures

View File

@@ -1,4 +1,4 @@
interface:
display_name: "OpenClaw Release CI"
short_description: "Verify and debug OpenClaw release validation runs"
default_prompt: "Use $openclaw-release-ci to preflight provider secrets, watch full release validation, summarize child runs, and triage only failing release lanes."
default_prompt: "Use $release-openclaw-ci to preflight provider secrets, watch full release validation, summarize child runs, and triage only failing release lanes."

View File

@@ -1,26 +1,23 @@
---
name: openclaw-mac-release
name: release-openclaw-mac
description: "Run or recover OpenClaw macOS release signing, notarization, appcast, and asset promotion."
---
# OpenClaw Mac Release
Use with `$openclaw-release-maintainer`, `$openclaw-release-ci`, and `$one-password` when stable macOS assets, private mac preflight, notarization, appcast promotion, or mac release recovery is involved.
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, private mac preflight, notarization, appcast promotion, or mac release recovery is involved.
## Credentials
- Canonical ASC item: vault `Molty`, title `API Key - App Store Connect - Personal - Release`.
- Resolve Peter-owned ASC item refs, key ids, issuer ids, and service-token provenance from `$release-private`.
- Fields: `private_key_p8`, `key_id`, `issuer_id`.
- Current known good key id: `AKVLXW849T`.
- Legacy mirror: vault `Private`, title `API Key - App Store Connect - Personal`; keep it synced for older refs.
- Stale/revoked key symptom: `xcrun notarytool submit` fails with `HTTP status code: 401. Unauthenticated`.
- Validate candidate ASC credentials with `xcrun notarytool history` before setting GitHub secrets.
## 1Password
- Use `$one-password`: all `op` work inside one persistent tmux session, no secret output.
- Prefer `OP_SERVICE_ACCOUNT_TOKEN` from `~/.profile` for Molty reads.
- Do not assume `MOLTY_OP_SERVICE_ACCOUNT_TOKEN` is alive; it has previously pointed at a deleted service account.
- Use the service-token guidance from `$release-private` when available.
- If a service token fails, run status-only checks: token present/length and `op whoami`; never print token values.
- If desktop app auth is needed but Touch ID is unavailable, set `OP_BIOMETRIC_UNLOCK_ENABLED=false` for the manual `op account add --signin` path.

View File

@@ -1,11 +1,11 @@
---
name: openclaw-release-maintainer
name: release-openclaw-maintainer
description: Prepare or verify OpenClaw stable/beta releases, changelogs, release notes, publish commands, and artifacts.
---
# OpenClaw Release Maintainer
Use this skill for release and publish-time workflow. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
Use this skill for release and publish-time workflow. Load `$release-private` if it exists before resolving Peter-owned credential locators or private host topology. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
## Respect release guardrails
@@ -23,7 +23,8 @@ Use this skill for release and publish-time workflow. Keep ordinary development
green. Then branch from that commit so regular development can continue on
`main` while release validation runs.
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
pull/rebase, then generate `CHANGELOG.md` on `main` from merged PRs and all
direct commits since the last reachable release tag. 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
@@ -59,8 +60,17 @@ Use this skill for release and publish-time workflow. Keep ordinary development
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.
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
with `target_ref=<release-sha>` in parallel with the other release work. Do
not wait for full release validation to start the performance signal.
- Before publish/closeout, compare available product performance metrics with
earlier releases: Kova agent-turn/resource metrics, gateway startup
ready/listen/RSS/CPU metrics, and CLI startup metrics from release evidence
or clawgrit reports. Report regressions explicitly. A major regression is a
release blocker unless the operator waives it or the data clearly proves
infrastructure noise.
- Generate the 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
stable base version section, for example `v2026.4.20-beta.1` uses
`## 2026.4.20` release notes.
@@ -127,11 +137,25 @@ Use this skill for release and publish-time workflow. Keep ordinary development
## Build changelog-backed release notes
- `CHANGELOG.md` is release-owned. Normal PRs and direct `main` fixes should
not edit it.
- Before release branching or tagging, rewrite the target `CHANGELOG.md`
section from commit history, not just from existing notes: scan commits since
the last reachable release tag, add missed user-facing changes, dedupe
overlapping entries, and sort each section from most to least interesting for
users.
section from history, not existing notes. Use the last reachable stable or
beta release tag as the base, then inspect every commit through the target
release SHA.
- Include both merged PR commits and direct commits on `main`. Direct commits
matter: infer notes from their subject, body, touched files, linked issues,
tests, and nearby code when no PR body exists.
- Prefer PR bodies, issue links, review proof, and commit bodies over commit
subjects alone. If a commit fixed an issue directly, the commit body should
name the user-visible behavior, affected surface, issue ref, and credited
reporter/contributor when known.
- Treat missing context as a release-note audit gap: inspect the diff and linked
issue, draft the best accurate entry, and note the uncertainty for maintainer
review rather than inventing impact.
- Add missed user-facing changes, remove internal-only noise, dedupe overlapping
PR/direct-commit entries, and sort each section from most to least interesting
for users.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies must use the full matching
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
@@ -412,7 +436,7 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Hard rule: never run `op` directly in the main agent shell during release
work. Any 1Password CLI use must happen inside that tmux session so prompts
and alerts are contained and observable.
- Use the 1Password item `op://Private/Npmjs` for npm credentials and OTP.
- Use `$release-private` for the npm credentials and OTP item.
Do not print passwords, tokens, or OTPs to the transcript; send them through
tmux buffers, env vars scoped to the tmux command, or `expect` with
`log_user 0`.
@@ -540,34 +564,42 @@ 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 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
9. Immediately dispatch Actions > `OpenClaw Performance` from `main` with
`target_ref=<release-sha>`, `profile=release`, `repeat=3`, deep profiling
off, live OpenAI off, and regression failure off. Let it run in parallel
with preflight and validation work.
10. 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.
11. 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.
11. Confirm the target npm version is not already published.
12. Create and push the git tag from the release branch.
13. Create or refresh the matching GitHub release.
14. Dispatch Actions > `QA-Lab - All Lanes` against the release tag and wait
12. Confirm the target npm version is not already published.
13. Create and push the git tag from the release branch.
14. Create or refresh the matching GitHub release.
15. Dispatch Actions > `QA-Lab - All Lanes` against the release tag and wait
for the mock parity, live Matrix, and live Telegram credentialed-channel
lanes to pass.
15. Start `.github/workflows/openclaw-npm-release.yml` from the release branch
16. Start `.github/workflows/openclaw-npm-release.yml` from the release branch
with `preflight_only=true`
and choose the intended `npm_dist_tag` (`beta` default; `latest` only for
an intentional direct stable publish). Wait for it to pass. Save that run id
because the real publish requires it to reuse the prepared npm tarball.
16. For stable releases, start `.github/workflows/macos-release.yml` in
17. Before real publish, review the early performance run if it has completed.
Compare against earlier release evidence or clawgrit reports where
available. Call out minor regressions in the release proof; block on major
regressions unless waived or proven noisy.
18. For stable releases, start `.github/workflows/macos-release.yml` in
`openclaw/openclaw` and wait for the public validation-only run to pass.
17. For stable releases, start
19. For stable releases, start
`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
with the same tag and wait for the private mac validation lane to pass.
18. For stable releases, start
20. For stable releases, start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
with `preflight_only=true` and wait for it to pass. Save that run id because
the real publish requires it to reuse the notarized mac artifacts.
19. If any preflight or validation run fails, fix the issue on a new commit,
21. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and matching GitHub release, recreate them from the fixed
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes. For pushed or
@@ -575,15 +607,15 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
For preflight-only failures where npm did not publish the beta version,
delete/recreate the same beta tag and prerelease at the fixed commit instead
of skipping a prerelease number.
20. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
22. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
the same tag for the real publish, choose `npm_dist_tag` (`beta` default,
`latest` only when you intentionally want direct stable publish), keep it
the same as the preflight run, and pass the successful npm
`preflight_run_id`.
21. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
22. Run postpublish verification:
23. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
24. Run postpublish verification:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
23. Run the post-published beta verification roster. First scan current `main`
25. 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
@@ -597,10 +629,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
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
authorized beta-attempt limit, normally 4.
24. Announce the beta/stable release on Discord best-effort using the configured secret workflow.
25. If the operator requested beta only, stop after beta verification and the
26. Announce the beta/stable release on Discord best-effort using the configured secret workflow.
27. If the operator requested beta only, stop after beta verification and the
announcement.
26. If the stable release was published to `beta`, use the light stable
28. If the stable release was published to `beta`, use the light stable
promotion roster when the matching beta already carried the full confidence
pass: published npm postpublish verify, Docker install/update smoke,
macOS-only Parallels install/update smoke, and required QA signal.
@@ -608,24 +640,24 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
`openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml`
workflow to promote that stable version from `beta` to `latest`, then
verify `latest` now points at that version.
27. If the stable release was published directly to `latest` and `beta` should
29. If the stable release was published directly to `latest` and `beta` should
follow it, start that same private dist-tag workflow to point `beta` at the
stable version, then verify both `latest` and `beta` point at that version.
28. For stable releases, start
30. For stable releases, start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
for the real publish with the successful private mac `preflight_run_id` and
wait for success.
29. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
31. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.
30. For stable releases, download `macos-appcast-<tag>` from the successful
32. For stable releases, download `macos-appcast-<tag>` from the successful
private mac run, update `appcast.xml` on `main`, and verify the feed. Merge
or cherry-pick release branch changes back to `main` after stable succeeds.
31. For beta releases, publish the mac assets only when intentionally requested;
33. For beta releases, publish the mac assets only when intentionally requested;
expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
32. After publish, verify npm and the attached release artifacts.
34. After publish, verify npm and the attached release artifacts.
## GHSA advisory work

View File

@@ -0,0 +1,288 @@
---
name: release-openclaw-nightly
description: "OpenClaw Tideclaw alpha/nightly release automation: isolated branches, local fixes, release CI, branch retention, and forward-port to main."
---
# Nightly Release
Use for Tideclaw/OpenClaw alpha/nightly release automation, manual alpha triggers, beta prep, release-branch repair, and post-release forward-port. Load `$release-private` if it exists before using Tideclaw host paths, cron ids, or Discord routing ids.
## Policy
- Alpha/nightly runs every 12h or by manual trigger.
- Beta is human-triggered from Discord from a proven alpha/release branch.
- Stable/latest always needs explicit human confirmation.
- Never publish from a dirty checkout or directly from `main`.
- Main can be busy or broken; alpha work must be isolated so transient main failures do not block a usable nightly.
- Publish only after release-branch proof is green.
- After a successful alpha, forward-port release-branch commits back to `main` and prove main CI green.
- Forward-port PRs contain only reusable fixes needed to make nightly/release checks pass. They must not contain alpha version bumps, release notes, changelog release entries, tags, generated artifacts, or state-file updates.
- Keep only alpha/nightly branches from the last 3 days, plus any branch with an active run, open PR, or release tag.
- Never run broad env/token dumps. For GitHub writes on the Tideclaw host, use the Tideclaw `gh` write wrapper below.
## Identity
Tideclaw should commit under its own machine identity on release branches and forward-port branches:
```bash
git config user.name "Tideclaw"
git config user.email "tideclaw@openclaw.ai"
```
This is good for auditability if commits are clearly machine-authored and gated by CI. Avoid direct pushes to protected `main`; forward-port via PR/automerge unless the repo policy explicitly allows the bot to push after green checks. Include human `Co-authored-by` only when a human supplied the patch or explicit commit text.
## Branch Shape
- Branch prefix: `tideclaw/alpha/`
- Branch name: `tideclaw/alpha/YYYY-MM-DD-HHMMZ`
- Base: current `origin/main` SHA at trigger time.
- State file: resolve from `$release-private` on the Tideclaw host.
- Release tag: `vYYYY.M.D-alpha.N`
- npm dist-tag: `alpha`
Do not reuse old alpha branches for a new run. If rerunning the same base SHA, create a new timestamped branch and record why.
## Start
1. Work in the Tideclaw host checkout from `$release-private`.
2. Fetch first:
```bash
git fetch origin main --tags --prune
git switch main
git merge --ff-only origin/main
BASE_SHA="$(git rev-parse origin/main)"
BRANCH="tideclaw/alpha/$(date -u +%Y-%m-%d-%H%MZ)"
git switch -c "$BRANCH" "$BASE_SHA"
```
3. Read repo release docs/scripts before changing anything:
- `AGENTS.md`
- release docs under `docs/`
- release scripts under `scripts/`
- `.github/workflows/*release*`
4. Compare `$BASE_SHA` with the last successful alpha state and current git/npm/GitHub alpha tags. If already released, report skip and do not publish.
Manual trigger:
```bash
CRON_ID="<from release-private>"
OPENCLAW_ALLOW_ROOT=1 openclaw cron run "$CRON_ID" --expect-final --timeout 21600000
```
## Discord Alpha Trigger
Tideclaw may run alpha immediately from Discord when a maintainer mentions Tideclaw in `#releases` or `#maintainers`.
Accepted shapes:
```text
@Tideclaw run alpha now
@Tideclaw alpha release from main now
@Tideclaw trigger alpha
```
Rules:
1. Treat this as a manual alpha trigger equivalent to the alpha cron job.
2. Start from current `origin/main` and create a fresh `tideclaw/alpha/YYYY-MM-DD-HHMMZ` branch.
3. Follow the normal alpha workflow: reuse prior fixes, run local checks, fix on the alpha branch, run release CI, publish alpha after green gates, then forward-port reusable fixes via fixes-only PR.
4. If another alpha/beta/stable release run is already active, report the active branch/run and stop.
5. `#maintainers` trigger requires an explicit Tideclaw mention; do not react to unmentioned release chatter there.
6. Resolve Discord role/user ids and live host hotfix notes from `$release-private`.
## Discord Beta Trigger
Tideclaw may run beta releases from `#releases` or mentioned `#maintainers` commands only when a maintainer sends an explicit beta trigger. Treat this as human approval for beta, not for stable/latest.
Accepted shapes:
```text
@Tideclaw beta release from vYYYY.M.D-alpha.N
@Tideclaw beta release from tideclaw/alpha/YYYY-MM-DD-HHMMZ
@Tideclaw beta release from latest proven alpha
```
Rules:
1. Require the words `beta release` and a source alpha tag/branch, or `latest proven alpha`.
2. If the source is ambiguous, ask one clarifying question in `#releases` and stop.
3. Verify the source alpha first: GitHub release, npm `alpha` package, release CI, recorded state file, and branch/tag SHA.
4. Create a fresh beta branch `tideclaw/beta/YYYY-MM-DD-HHMMZ` from the proven alpha source, not directly from a moving `main`.
5. Reuse/squash only stabilization fixes already proven on alpha. Do not import unrelated alpha release mechanics unless the beta release docs require them.
6. Compute beta as `vYYYY.M.D-beta.N`, matching npm `--tag beta`.
7. Run beta release validation/preflight/full release CI and fix failures on the beta branch.
8. Publish beta only after green beta gates. Use GitHub Actions/OIDC, never direct npm publish from the host.
9. Final Discord summary must include source alpha, beta tag/version, branch, fix commits, workflow run IDs, npm/GitHub proof, and any skipped/blocked reason.
10. After beta publishes, forward-port reusable fixes to `main` using the same fixes-only PR rules below.
## Reuse Prior Fixes
Before running checks, mine recent Tideclaw alpha branches for fixes already made during previous release attempts:
1. Read the Tideclaw state file from `$release-private` for the last successful alpha branch and fix commit SHAs.
2. List recent remote branches:
```bash
git for-each-ref refs/remotes/origin/tideclaw/alpha --format='%(refname:short) %(committerdate:iso-strict)'
```
3. Consider only Tideclaw alpha branches from the last 3 days plus the last successful alpha branch.
4. For each candidate branch, inspect commits that are not in current `origin/main`:
```bash
git log --no-merges --reverse --format='%H%x09%s' origin/main..origin/tideclaw/alpha/YYYY-MM-DD-HHMMZ
```
5. Cherry-pick only real stabilization fixes that still apply to the new alpha branch. Prefer commits recorded as `fixCommitShas` in the state file.
6. Skip version bumps, changelog release entries, tag artifacts, generated release notes, state-file-only commits, and one-off debug instrumentation.
7. If a cherry-pick conflicts, inspect whether current main already contains an equivalent fix. If not, resolve minimally and keep the commit message clear.
8. Record reused commit SHAs separately from newly authored fix SHAs in the alpha state and final Discord summary.
Use `git cherry`, `git range-diff`, and targeted test reruns to avoid duplicating fixes already present on `main`.
## Repair Loop
Use the branch as a release-candidate repair surface:
1. Run narrow local checks first: changed tests, release preflight, type/lint/build gates required by release docs.
2. If local checks fail, fix on the alpha branch with minimal commits.
3. Commit each coherent fix as Tideclaw.
4. Re-run the failed local check after each fix.
5. Do not hide failures by editing baselines, expected-failure lists, ignore files, or release inventory unless the release docs explicitly require it and the diff is justified.
6. If a failure is flaky, rerun once; if still red, treat it as real.
7. If the fix is clearly useful for main, keep it small and forward-portable. Avoid broad refactors during alpha stabilization.
Commit examples:
```bash
git add <files>
git commit -m "fix: stabilize alpha release preflight"
git push -u origin "$BRANCH"
```
## Release CI
After local proof:
1. Compute the next `vYYYY.M.D-alpha.N` from existing git tags, npm versions, and GitHub releases.
2. Make the alpha branch package version and release metadata match that tag, commit it, and push the branch.
3. Run release validation from the alpha branch, using GitHub CLI, not browser/fetch tools. On the Tideclaw host, bare `gh` is a read-only Codex sandbox wrapper; use `/usr/local/bin/gh-tideclaw-write` for write-capable commands such as `workflow run`, `run cancel`, and publish dispatch:
```bash
GH="/usr/local/bin/gh-tideclaw-write"
SHA="$(git rev-parse HEAD)"
TAG="v$(node -p "require('./package.json').version")"
BRANCH="$(git branch --show-current)"
"$GH" workflow run full-release-validation.yml --repo openclaw/openclaw --ref "$BRANCH" \
-f ref="$BRANCH" \
-f release_profile=beta \
-f rerun_group=all
"$GH" workflow run openclaw-npm-release.yml --repo openclaw/openclaw --ref "$BRANCH" \
-f tag="$SHA" \
-f preflight_only=true \
-f npm_dist_tag=alpha
```
4. Watch the exact workflow run IDs and head SHA with `gh run list`, `gh run view`, and `gh api`. Read-only `gh` is fine for polling; use `$GH` only when a command mutates GitHub. Do not use Codex browser/fetch for GitHub API polling; prior Tideclaw runs failed there after successful preflight.
5. For alpha, blocking gates are the ones Tideclaw can repair directly or that prove package safety: normal CI, plugin prerelease, npm preflight, package preparation, install smoke, tag/reachability, and publish verification. Treat cross-OS, live channel, QA Lab, package acceptance, long Docker E2E, and Telegram package E2E failures as advisory; report them in Discord and continue if the blocking gates are green.
- If `rerun_group=all` is stuck only on advisory lanes after CI, plugin prerelease, npm preflight, package preparation, and install smoke are green, dispatch a focused Full Release Validation on the same head with `-f rerun_group=install-smoke`. Use that successful focused Full Release Validation run as the publish proof, and include the separate CI/plugin/full advisory run IDs in the Discord summary.
6. If a blocking gate fails, fix on the alpha branch, push, and rerun only the failed or required release CI. If the commit changes, discard old preflight/full-validation run IDs and rerun them for the new head.
7. After full validation and npm preflight are green on the same branch head, create and push the release tag from that exact commit:
```bash
git tag -a "$TAG" "$SHA" -m "openclaw ${TAG#v}"
git push origin "$TAG"
```
8. Dispatch the publish wrapper from the same alpha branch. Use the successful npm preflight run ID and full release validation run ID from the same head SHA:
```bash
"$GH" workflow run openclaw-release-publish.yml --repo openclaw/openclaw --ref "$BRANCH" \
-f tag="$TAG" \
-f preflight_run_id="$NPM_PREFLIGHT_RUN_ID" \
-f full_release_validation_run_id="$FULL_RELEASE_VALIDATION_RUN_ID" \
-f npm_dist_tag=alpha \
-f plugin_publish_scope=all-publishable \
-f publish_openclaw_npm=true \
-f release_profile=beta \
-f wait_for_clawhub=false
```
9. Watch the publish wrapper plus child runs. If `openclaw-npm-release.yml` is waiting on the `npm-release` environment and Tideclaw cannot approve it, report that as the only blocker; do not call the release done.
10. Do not publish npm directly from the host; use GitHub Actions/OIDC.
Important: `openclaw-npm-release.yml` with `preflight_only=true` only prepares artifacts. It does not publish. A successful alpha requires the later `openclaw-release-publish.yml` wrapper, a pushed git tag, npm `alpha` dist-tag proof, and a GitHub prerelease.
## Verify Published Alpha
Release is not done until all are true:
- GitHub tag exists.
- GitHub Release exists and is marked prerelease.
- Release body links npm version page, registry tarball, integrity, and CI/proof.
- `npm view openclaw@<version>` shows the exact version, dist-tag `alpha`, tarball, integrity, and publish time.
- Installed/package smoke follows repo release docs.
- The Tideclaw state file from `$release-private` records version, tag, base SHA, branch, fix commit SHAs, workflow run IDs, npm integrity, and timestamp.
Final Discord summary in `#releases`:
- tag/version
- base SHA
- branch
- fix commits
- workflow run IDs
- npm/GitHub proof
- skipped/blocked reason if not released
Use Discord-safe Markdown links with angle-bracket targets. Never print secrets.
## Forward-Port
After a successful alpha, raise a fixes-only PR back to `main`:
1. Create/update a forward-port branch from current `origin/main`:
```bash
git fetch origin main --prune
git switch -c "tideclaw/forward-port/$(date -u +%Y-%m-%d-%H%MZ)" origin/main
```
2. Cherry-pick only release-branch commits that are real fixes required to make nightly/release checks pass.
3. Exclude alpha version bumps, changelog release entries, release notes, tag artifacts, generated release assets, state-file-only commits, and any commit whose only purpose was publishing the alpha.
4. If a commit mixes a real fix with release/version changes, split it: replay only the fix hunks into a new commit on the forward-port branch.
5. Resolve conflicts in favor of the minimal main-compatible fix.
6. Run the relevant changed/local gate.
7. Push and open a PR, or use the repos allowed bot merge path.
8. Wait for required main CI to go green. If CI fails, fix on the forward-port branch and rerun.
9. Report the PR/merge SHA and any commits intentionally not forward-ported.
If `origin/main` is independently red before the forward-port, document the unrelated failing check and still keep the forward-port PR green against its head when possible.
## Branch Retention
Before and after each run, prune old alpha branches:
1. List `origin/tideclaw/alpha/*`.
2. Keep branches whose timestamp is within the last 3 days UTC.
3. Keep branches referenced by a live workflow run, open PR, release tag, or state file.
4. Delete only Tideclaw-owned alpha branches:
```bash
git push origin --delete tideclaw/alpha/YYYY-MM-DD-HHMMZ
```
Never delete human branches, beta branches, stable branches, or unknown prefixes.
## Stop Conditions
Stop and report clearly if:
- release docs/scripts disagree on versioning or publish path
- required secrets/auth are unavailable
- GitHub Actions cannot be dispatched or observed
- a required release gate stays red after a real fix attempt
- npm/GitHub state disagrees after publish
- forward-port cannot be made green without a larger product decision

View File

@@ -1,5 +1,5 @@
---
name: openclaw-pre-release-plugin-testing
name: release-openclaw-plugin-testing
description: Plan and run pre-release OpenClaw plugin validation across bundled plugins, package artifacts, lifecycle commands, doctor/fix, config round-trip, gateway startup, SDK compatibility, Docker E2E, Package Acceptance, and Testbox proof.
---

View File

@@ -0,0 +1,4 @@
interface:
display_name: "OpenClaw Plugin Pre-Release Testing"
short_description: "Plan plugin release validation"
default_prompt: "Use $release-openclaw-plugin-testing to plan or run pre-release OpenClaw plugin validation across package, lifecycle, doctor, gateway, SDK, and live-ish proof."

View File

@@ -0,0 +1,79 @@
---
name: technical-documentation
description: Build and review high-quality technical docs as well as agent instruction files in your repository.
license: MIT
metadata:
source: "https://github.com/vincentkoc/dotskills"
---
# Technical Documentation
## Purpose
Produce and review technical documentation that is clear, actionable, and maintainable for both humans and agents, including contributor-governance files and agent instruction files.
## When to use
- Creating or overhauling docs in an existing product/codebase (brownfield).
- Building evergreen docs meant to stay accurate and reusable over time.
- Reviewing doc diffs for structure, clarity, and operational correctness.
- Running full-repo documentation audits that must include both governance files and product docs surfaces (`docs/`, `README*`, `.md/.mdx/.mdc`, Fern/Sphinx/Mintlify-style sources).
- Updating or reviewing AGENTS.md and/or CONTRIBUTING.md to keep agent and contributor workflows aligned with current repo practices.
- Improving repository onboarding/docs that include contribution instructions, issue templates, PR flow, and review gates.
- Designing governance documentation strategy for repos with alias instruction files (for example `CLAUDE.md`, `AGENT.md`, `.cursorrules`, `.cursor/rules/*`, `.agent/`, `.agents/`, `.pi/`) where `AGENTS.md` is treated as canonical when present and aliases should be kept as compatibility surfaces.
- Diagnosing agent-file drift where teams had to prompt iteratively to surface missing files, broken commands, or policy conflicts.
- Applying repository-specific documentation overlays, including OpenClaw page-type, docs IA, preservation, and validation rules when present.
## Workflow
1. Classify task: `build` or `review`; context: `brownfield` or `evergreen`.
2. Inventory full documentation scope early (governance + product docs): AGENTS/CONTRIBUTING/aliases plus docs directories, framework sources, and root/module READMEs.
3. Detect multilingual scope (README/docs in multiple languages) and define required parity level.
4. Read `references/agent-and-contributing.md` for agent instruction and `CONTRIBUTING.md` workflow rules (inventory, canonical/alias mapping, dual-mode balance, deliverable standards, and precedence/conflict handling).
5. Read `references/principles.md` for the governing ruleset (Matt Palmer & OpenAI).
6. For OpenClaw docs work, read `references/openclaw.md` before the build/review playbook.
7. For build tasks, follow `references/build.md`.
8. For review tasks, follow `references/review.md` and proactively detect issues without waiting for repeated prompts.
9. For complex or high-risk tasks (build or review), it is acceptable to run longer, deeper, and more exhaustive investigations when needed for confidence.
10. When available, use sub-agents for bounded parallel discovery/review work, then merge outputs into one coherent final deliverable.
11. Use `references/tooling.md` when platform/tooling choices affect recommendations.
12. Run a proactive issue sweep for both governance and docs-content surfaces, and fix high-confidence defects in the same pass unless explicitly asked for report-only mode.
13. In brownfield mode, prioritize compatibility with current docs IA, tooling, and release state.
14. In evergreen mode, prioritize timeless wording, update strategy, and durable structure.
15. Return deliverables plus validation notes, parity status, and remaining gaps.
## Sub-agent orchestration guidance
Prefer sub-agents when the repo is large or the requested change set is broad; use them by default for repo-wide, multi-framework, or high-conflict work.
- `inventory-agent` -> `agents/inventory-agent.md` (`fast` / Claude `haiku`): file/config discovery, coverage map, and missing-path checks.
- `governance-agent` -> `agents/governance-agent.md` (`thinking` / Claude `sonnet`): AGENTS/CONTRIBUTING/alias precedence, conflicts, and policy drift.
- `docs-framework-agent` -> `agents/docs-framework-agent.md` (`thinking` / Claude `sonnet`): framework config, relative path base, and file-path vs URL-path mapping checks.
- `synthesis-agent` -> `agents/synthesis-agent.md` (`long` / Claude `opus`): merge sub-agent outputs into one prioritized fix plan and unified precedence model.
## Inputs
- Doc type (tutorial, how-to, reference, explanation) and audience.
- File scope or diff scope.
- Docs framework/tooling constraints (Fern, Mintlify, Sphinx, etc.).
- Build/review mode and brownfield/evergreen intent.
- Target agent and human compatibility intent.
- Docs framework surfaces in scope (for example Fern, Sphinx, Mintlify, Markdown/MDX/MDC/RST/RSC files).
- Desired investigation depth/time budget (quick pass vs exhaustive review).
- Execution mode (`single-agent` or `sub-agent-assisted` when available).
- Remediation mode (`apply-fixes` by default, or `report-only` when requested).
- Multilingual scope: source-of-truth language, target locales, and parity expectations.
- Repository-specific overlay constraints, if any.
## Outputs
- Updated draft or review findings with clear next actions.
- Validation notes (what was checked, what remains).
- Navigation/maintenance recommendations for long-term quality.
- Governance-doc alignment summary when AGENTS/CONTRIBUTING were touched.
- Agent instruction-surface map (primary file, alias files, Codex/Claude/Cursor handling plan).
- Documentation-surface coverage map (what was reviewed under `/docs`, README hierarchy, and framework-specific source trees).
- Autodetected issue list with applied fixes (or explicit report-only findings).
- Delegation notes when sub-agents were used (scope delegated and how findings were merged).
- Multilingual parity note (in-sync, partial with rationale, or intentionally divergent).
- Repository-specific overlay notes when one was used.

View File

@@ -0,0 +1,32 @@
---
name: docs-framework-agent
description: Thinking-focused docs framework checker for config-relative paths and route/file mapping consistency.
model: sonnet
tools:
- Read
- Glob
- Grep
permissionMode: default
maxTurns: 10
---
You are the docs-framework sub-agent for technical documentation.
Goals:
- validate framework config-driven docs behavior
- prevent path-mapping drift between source files and published routes
Tasks:
- detect and read framework config first (Fern/Sphinx/Mintlify/custom)
- resolve paths relative to the declaring file/config
- validate both maps:
- config -> file exists
- config/nav/routing -> URL path is valid and consistent
Return:
- config files reviewed
- path assumptions made
- mismatches (`missing file`, `stale route`, `wrong base path`)

View File

@@ -0,0 +1,30 @@
---
name: governance-agent
description: Thinking-focused governance reviewer for AGENTS/CONTRIBUTING/alias precedence, conflict detection, and policy drift analysis.
model: sonnet
tools:
- Read
- Glob
- Grep
permissionMode: default
maxTurns: 10
---
You are the governance sub-agent for technical documentation.
Goals:
- validate AGENTS/CONTRIBUTING/alias alignment and precedence
- identify policy drift and conflicting instructions
Tasks:
- determine canonical instruction source and alias compatibility mapping
- detect conflicts across nested scope files and tool-specific rule consumers
- validate command examples against stated governance expectations
Return:
- precedence model
- conflict list with severity
- recommended low-risk remediations

View File

@@ -0,0 +1,31 @@
---
name: inventory-agent
description: Fast repo-surface discovery for technical documentation audits. Use for coverage mapping and missing-path detection before deeper review.
model: haiku
tools:
- Read
- Glob
- Grep
- LS
permissionMode: default
maxTurns: 6
---
You are the inventory sub-agent for technical documentation.
Goals:
- enumerate governance and docs-content surfaces in scope
- detect missing files, broken references, and obvious command/path failures
Tasks:
- map `AGENTS.md`/`CONTRIBUTING.md`/aliases and docs surfaces (`docs/**`, README hierarchy, `.md/.mdx/.mdc/.rst/.rsc`)
- list framework config files discovered (Fern/Sphinx/Mintlify or equivalent)
- report hard failures only, with exact file paths
Return:
- coverage map
- missing/broken path list
- unresolved blockers

View File

@@ -0,0 +1,10 @@
interface:
display_name: "Technical Documentation"
short_description: "Build and review technical documentation for brownfield and evergreen systems."
icon_small: "./assets/icon.jpg"
icon_large: "./assets/icon.jpg"
brand_color: "#111827"
default_prompt: "Build or review technical documentation with a clear, maintainable, and production-ready workflow."
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,28 @@
---
name: synthesis-agent
description: Long-context synthesis agent that merges sub-agent outputs into one prioritized and deduplicated documentation action plan.
model: opus
tools:
- Read
permissionMode: default
maxTurns: 12
---
You are the synthesis sub-agent for technical documentation.
Goal:
- merge sub-agent outputs into one coherent, non-duplicated action plan
Tasks:
- prioritize blockers first, then non-blocking improvements
- normalize to one precedence model for governance decisions
- remove duplicated recommendations and contradictory fixes
- keep final output concise and execution-ready
Return:
- prioritized fix plan
- validation summary (done vs pending)
- explicit remaining gaps/blockers

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,145 @@
# AGENT and CONTRIBUTING Principles
This reference consolidates the core rules for agent-policy and contributor-governance docs.
You must:
1. Discover repo-level and nested instruction files with:
`rg --files -g 'AGENTS.md' -g 'CONTRIBUTING.md' -g 'CLAUDE.md' -g 'AGENT.md' -g '.cursor/rules/*' -g '.cursorrules' -g '.agent/**' -g '.agents/**' -g '.pi/**' -g 'AGENTS.*.md'`
2. Read the root and nearest-scope `AGENTS.md`/`CONTRIBUTING.md` pair before editing.
3. If alias files exist, normalize to one canonical source (`AGENTS.md` preferred when present; otherwise nearest alias), plus compatibility pointers or explicit symlink notes.
4. Document conflicting instructions and precedence decisions.
## GitHub + AGENTS baseline
Source: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors
Source: https://agents.md/
Source: https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/
Source: https://cobusgreyling.substack.com/p/what-is-agentsmd
Source: https://www.infoq.com/news/2025/08/agents-md/
Use these as default operating principles:
1. Keep `CONTRIBUTING.md` discoverable and actionable (`.github`, root, or `docs`).
2. Keep agent instructions concrete: real commands, real paths, clear boundaries.
3. Use explicit behavior boundaries for agents: `Always`, `Ask first`, `Never`.
4. Keep contributor and agent rules aligned with actual repository workflows.
5. Ensure clear guidance is provided to agents on if, when and how to raise issues and pull requests.
## Canonical and alias policy
Source: https://agents.md/
Source: https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/
1. Treat `AGENTS.md` as canonical when present.
2. If `AGENTS.md` is absent, treat the nearest alias file as canonical.
3. Keep compatibility surfaces explicit: `AGENTS.md`, `AGENT.md`, `.cursorrules`, `.cursor/rules/*`, `.agent/`, `.agents/`, `.pi/`.
4. If aliases are used, document how they map back to canonical policy (or symlink when supported).
5. When repos use `.agents/` as canonical rule storage, keep `.cursor` as a compatibility symlink to `.agents` for Cursor rule auto-loading.
6. Keep policy DRY: store one shared policy core and expose it via aliases/symlinks instead of duplicating rule text.
## Context-awareness by agent platform
Source: https://github.com/vercel-labs/agent-skills/blob/main/AGENTS.md
Source: https://github.com/openai/codex/blob/main/AGENTS.md
1. For Cursor and Claude-style glob consumers, keep rule files narrow and bounded.
2. Avoid over-referencing large path sets that inflate context for glob-based agents.
3. For Codex-style workflows, prefer explicit file references and deterministic commands.
4. Keep long runbooks outside top-level policy files; link to scoped docs.
5. Ensure all agents have a happy path regardless so ensuring everything works across Codex, Claude and other coding agents.
## Symlink and compatibility operations
1. Preferred layout for multi-agent compatibility:
- canonical rule directory: `.agents/`
- Cursor compatibility path: `.cursor -> .agents` symlink
- canonical policy doc: `AGENTS.md` pointing to `.agents` paths where relevant
2. Validate symlink state before finalizing changes:
- if `.agents/` exists and `.cursor` is missing, create `.cursor` symlink to `.agents`
- if `.cursor` is a symlink to another target, fix target or document why it must differ
- if `.cursor` is a real directory/file, treat as migration conflict and ask before replacement
3. Validate rule payload through the canonical directory:
- rules: `.agents/rules/*.mdc` with valid frontmatter (`description`, `globs`, `alwaysApply` as needed)
- commands: `.agents/commands/*.md` when command routing is used
- MCP config: `.agents/mcp.json` when MCP is in scope
4. Keep Codex behavior explicit:
- `AGENTS.md` is primary for Codex repository instructions
- `.cursor` compatibility is for Cursor auto-loading and does not replace canonical AGENTS policy
5. Record applied symlink fixes and unresolved compatibility gaps in validation notes.
## Dual-mode and deliverable standards
Source: https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/
Source: https://agents.md/
Source: https://github.com/openai/codex/blob/main/AGENTS.md
Source: https://github.com/vercel-labs/agent-skills/blob/main/AGENTS.md
1. Author one shared policy core (same commands, boundaries, and precedence) for all agents.
2. For Cursor/Claude-style agents, expose that core through glob-driven and bounded files (small `AGENTS.md`/rule surface).
3. For Codex, expose that same core through explicit file references with precise scope.
4. Where styles diverge, prefer the smallest common structure that satisfies both and avoid duplicating policy text.
5. Treat AGENTS/CONTRIBUTING as first-class deliverables when in scope.
6. Preserve required structure, constraints, and examples from existing files.
7. Align wording and commands with active repository instructions.
## Proactive issue discovery and remediation
Source: https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/
Source: https://github.com/openai/codex/blob/main/AGENTS.md
Source: https://github.com/vercel-labs/agent-skills/blob/main/AGENTS.md
1. Run a conflict matrix review across AGENTS/aliases/CONTRIBUTING and related command/rule docs before finalizing.
2. Treat the following as high-priority defects: missing referenced files, non-existent setup commands, command scope mismatches, and branch/commit policy conflicts.
3. Do not stop at caveat-only notes when a low-risk fix is clear; apply the fix in the same pass.
4. If a canonical entry file is missing (for example a directory `README.md` that docs depend on), create a minimal actionable file and update references.
5. Long-running investigations are acceptable when needed to uncover cross-file drift, especially in agent-instruction ecosystems.
## Discovery
1. Agents prefer simple terminal commands so having a well defined `make *` or `npm run *` is ideal
2. Agents can discover terminal commands through shell completion so providing shell completion helps
## CONTRIBUTING size and scope control
Source: https://contributing.md/how-to-build-contributing-md/
Source: https://blog.codacy.com/best-practices-to-manage-an-open-source-project
Source: https://mozillascience.github.io/working-open-workshop/contributing/
Source: https://github.com/openclaw/openclaw/blob/main/CONTRIBUTING.md
1. Keep root `CONTRIBUTING.md` focused on setup, issue flow, PR flow, testing, and review gates.
2. Use issue/PR template links instead of embedding every process detail inline.
3. When the file grows too large, split by domain and link from root.
4. Move any large content into docs if avalible (for example Mintlify/Fern/Sphinx workflows) to avoid large contributor guide.
5. Optimize for agent/machine readability as well as humans.
## Example repos to emulate
Source: https://github.com/openclaw/openclaw/blob/main/AGENTS.md
Source: https://github.com/openclaw/openclaw/blob/main/CONTRIBUTING.md
Source: https://github.com/openclaw/openclaw/blob/main/VISION.md
Source: https://github.com/openai/codex/blob/main/AGENTS.md
Source: https://github.com/processing/p5.js/blob/main/AGENTS.md
Source: https://github.com/vercel-labs/agent-skills/blob/main/AGENTS.md
Source: https://github.com/agentsmd/agents.md/blob/main/AGENTS.md
Source: https://github.com/rails/rails/blob/main/CONTRIBUTING.md
Source: https://github.com/kubernetes/kubernetes/blob/master/CONTRIBUTING.md
Source: https://github.com/atom/atom/blob/master/CONTRIBUTING.md
Source: https://github.com/github/docs/blob/main/CONTRIBUTING.md
Source: https://github.com/facebook/react/blob/main/CONTRIBUTING.md
1. OpenClaw: strong real-world alias policy and AGENTS/CONTRIBUTING/VISION cohesion.
2. OpenAI Codex: strict command discipline and explicit scope control.
3. p5.js: explicit AI-policy guardrails in agent instructions.
4. Vercel + agentsmd spec: compact, context-efficient AGENTS patterns.
5. Rails/Kubernetes/Atom/GitHub Docs/React: contributor guidance patterns at different project scales.
## Practical merge policy
When these rules conflict:
1. Preserve contributor and reader task success first.
2. Preserve instruction clarity and unambiguous boundaries second.
3. Preserve long-term maintainability and context-efficiency third.
4. Add extra agent optimization only if it does not reduce human clarity or there is explict need.
5. Use your judgement as the expert.

View File

@@ -0,0 +1,116 @@
# Build Docs Playbook
Read `principles.md` first, then follow this execution flow.
## 1. Detect and align agent instruction and governance instructions
- Use `references/agent-and-contributing.md` as the source of truth for inventory, canonical/alias mapping, and precedence/conflict handling.
- Apply the symlink compatibility policy when in scope (`.agents` canonical directory with `.cursor` compatibility symlink when required by tooling).
- Long-running and extensive build investigations are acceptable when needed to resolve ambiguous or conflicting documentation sources.
- When available, use sub-agents for bounded parallel inventory/cross-check tasks and merge results into one canonical decision set.
- Capture required constraints before writing:
- nested-agent rules, command/test requirements, PR workflow, and style checks.
- Use the same command and validation expectations in proposed snippets and examples.
## 2. Inventory product documentation surfaces (not governance only)
- For repo-wide builds, include docs content surfaces in addition to AGENTS/CONTRIBUTING.
- Inventory docs files and frameworks in scope (examples): `README*.md`, `docs/**`, `**/*.md`, `**/*.mdx`, `**/*.mdc`, `**/*.rst`, `**/*.rsc`, Fern/Mintlify config, Sphinx `conf.py`.
- Build a coverage map before drafting so governance and product docs are both represented.
- If scope is ambiguous, default to broader docs discovery first, then narrow intentionally.
## 3. Framework config and path mapping rules
- Detect framework/config first (for example Fern config, Sphinx `conf.py`, Mintlify config, or equivalent).
- Resolve every referenced path relative to the file/config that declares it, not assumed repo root.
- Treat filesystem paths and published URL routes as separate mappings; do not infer one from the other without config evidence.
- Validate both layers:
- config -> file exists on disk
- config/nav/routing -> URL path is consistent and reachable
- Record path-mapping assumptions and mismatches in handoff (`missing file`, `stale route`, `wrong base path`).
## 4. Define intent and success
- Audience, prerequisites, and job-to-be-done.
- Expected reader outcome immediately after completion.
- Doc type: tutorial, how-to, reference, explanation.
- Success criteria: what must be true after publish.
## 5. Build structure before prose
- Follow the funnel: what/why, quickstart, next steps.
- Keep headings informative and scannable.
- Open each section with the takeaway sentence.
- Add decision points with concrete branch guidance.
- For OpenClaw docs work, choose a page type from `references/openclaw.md` before drafting.
- Keep task-critical OpenClaw configuration inline; link exhaustive defaults, enums, schemas, generated references, and rare debugging workflows.
## 6. Build AGENTS.md and CONTRIBUTING.md intentionally
- Keep AGENTS.md structure consistent with `agents.md` ecosystem patterns:
- include YAML frontmatter when present in repo style (`name`, `description`).
- state persona scope and explicit instruction boundaries: `Always`, `Ask first`, `Never`.
- include concrete commands and representative code examples.
- For CONTRIBUTING.md, prioritize issue triage flow, PR expectations, setup/test commands, and review gates.
- Add `Code of Conduct`, `Testing`, `Local checks`, and `PR expectations` sections when missing but required by the repo.
- If CONTRIBUTING.md is becoming too large, split by scope into linked docs (for example, framework/tool-specific setup and release workflows) and keep the root file as a concise entry point.
- Keep cross-file consistency: links from CONTRIBUTING.md to AGENTS.md (and vice versa) should be accurate and non-circular.
- If multiple AGENTS.md files exist, document the directory-level scope and avoid conflicting advice.
- If a required canonical entry file is missing (for example referenced `README.md` under a major directory), create the file in the same pass instead of adding a caveat-only note.
- For new entry files, keep them minimal and actionable: purpose, prerequisites, concrete run commands, and pointers to deeper docs.
## 7. Keep agent context tight
- Author once, expose twice:
- keep one shared policy core and avoid duplicating guidance in separate agent-specific files.
- publish that core through bounded glob-friendly files for Cursor/Claude plus explicit path references for Codex.
- For Cursor and Claude-style agents, avoid broad references. Use minimal globbing and narrow rule files that each serve one concern (for example, repo-wide setup, test rules, security checks).
- Keep AGENTS and alias files short-to-medium; move detailed runbooks to linked docs.
- For Codex, prefer explicit file references and concrete paths for exact reuse.
- Avoid adding unrelated historical or process details to avoid token/context drift during future tool reads.
## 8. Brownfield build mode
- Match existing terminology, navigation, and component patterns.
- Preserve existing IA unless there is a documented migration plan.
- For rewrites, include a migration note from old to new paths.
- Prefer smallest safe change set that improves utility.
## 9. Evergreen build mode
- Prefer stable concepts over release-tied narrative.
- Isolate volatile details under clearly marked version sections.
- Include maintenance signals: owners, refresh triggers, stale criteria.
- Include lifecycle notes: deprecation and replacement paths.
## 10. Writing constraints
- Use precise language and short, imperative instructions.
- Keep code examples copy-ready and self-contained.
- Include common failure modes and safe defaults.
- Avoid placeholder guidance that cannot be executed.
## 11. Agent and automation readiness
- Keep key facts in text (not image-only).
- Prefer structured lists/tables when choices matter.
- Add links and anchors that allow deterministic navigation.
- Document what can be checked automatically in CI.
## 12. Build validation
- Validate commands and snippets where possible.
- Verify links and references in changed sections.
- Run a reference existence sweep for every path/command you introduced.
- Verify docs-framework consistency when in scope (for example Sphinx/Fern config and referenced doc paths).
- For OpenClaw docs work, apply the validation checklist in `references/openclaw.md`.
## 13. Multilingual parity mode (when applicable)
- Pick one source-of-truth language for technical accuracy and release timing.
- Define parity target: full parity, staged parity, or intentional divergence per section.
- Keep structure aligned across locales (headings, anchors, section order) when possible.
- Preserve command/code correctness first; localize explanatory text second.
- If parity is not feasible, add a visible note with missing scope and expected sync window.
- Run a locale parity check for changed sections (added/removed steps, warnings, prerequisites).
- Record unresolved checks explicitly in handoff.

View File

@@ -0,0 +1,128 @@
# OpenClaw Documentation Overlay
Use this reference only for OpenClaw docs work. It layers OpenClaw-specific page
types, navigation, preservation, and validation rules on top of the general
technical-documentation skill.
## Reader Model
- Lead with the task the reader is trying to complete.
- Give one recommended path before alternatives.
- Keep main docs focused on the common path; move dense contracts and rare
debugging detail to linked reference or troubleshooting pages.
- Explain production risks exactly where the reader can make the mistake.
- Link concepts, guides, references, CLI pages, SDK docs, testing, and
troubleshooting so readers can continue without rereading.
## Page Types
Choose the page type before writing or reviewing:
- Overview: route readers to the right product area, integration path, or guide.
- Quickstart: get a new user to a working result with the fewest safe steps.
- Topic page: explain a major OpenClaw entity or surface end to end.
- Guide: walk through one workflow from prerequisites to production readiness.
- API/SDK/CLI reference: define every object, method, command, option, response,
error, enum, default, and version rule in scope.
- Testing guide: show sandbox setup, fixtures, simulated failures, and live-mode
differences.
- Troubleshooting guide: map observable symptoms to checks, causes, and fixes.
- Governance file: keep agent/contributor policy concrete, scoped, and aligned
with current OpenClaw repo behavior.
## Topic Pages
Use this shape for major-entity pages:
1. Title naming the entity or surface.
2. Unheaded opening that says what it is, what it owns, and what it does not own.
3. Requirements, only when setup needs accounts, versions, permissions, plugins,
operating systems, or credentials.
4. Quickstart with the recommended path and smallest reliable verification.
5. Configuration with task-critical options inline and exhaustive details linked
to reference docs.
6. Major subtopics organized by reader intent, not under a generic "Subtopics"
heading.
7. Troubleshooting with observable failures and concrete checks.
8. Related links to guides, references, commands, concepts, and adjacent topics.
## Guides
Use this shape for workflow pages:
1. Title naming the outcome, not the implementation detail.
2. Opening that states what the reader can accomplish.
3. Before you begin: accounts, keys, permissions, versions, tools, and
assumptions.
4. Choose a path, only when the reader must decide.
5. Steps with verb-led headings, commands, expected output, and checks.
6. Test with the smallest reliable proof that the workflow works.
7. Production readiness: security, retries, limits, observability, migrations,
and cleanup.
8. Troubleshooting near the workflow that causes the failures.
9. See also links to concepts, references, SDK docs, and adjacent guides.
## Docs IA And Navigation
- Read `docs/docs.json` before navigation changes.
- Keep topic pages and common workflows on the main reader path.
- Put exhaustive contracts, generated references, maintainer-only detail, and
support material under `Reference` or another clearly scoped support page.
- Keep generated `plugins/reference/*` children and redirect-only pages out of
visible navigation unless explicitly required.
- For moved pages, include a keep/drop/move/destination matrix in the handoff.
- Add "Read when" hints for docs-list routing when creating or changing pages
that participate in the docs index.
## Source-Backed Content
- CLI docs must match current flags, output, errors, and examples.
- API/SDK docs must include fields, defaults, enum values, constraints, nullable
behavior, lifecycle states, errors, and recovery guidance.
- Config docs must align exported types, schema/help output, metadata, baselines,
and current docs.
- Dependency-backed behavior must be verified from upstream docs, source, or
types before documenting defaults, timing, errors, or API behavior.
- Separate current behavior, shipped behavior, planned behavior, and maintainer
intent.
## Examples
- Prefer complete copy-pasteable commands and snippets.
- Use realistic variable names and values.
- Mark placeholders with angle-bracket names such as `<API_KEY>`.
- Show expected success output when it helps verification.
- Keep one conceptual unit per code block and use language-specific fences.
- Avoid examples that hide setup, auth, error handling, or cleanup.
- Never expose real secrets, live config, phone numbers, private videos, or
credentials.
## Preservation Reviews
For rewrites or splits:
- Identify source units before rewriting: headings, paragraphs, tables, examples,
CLI/API contracts, warnings, and troubleshooting facts.
- Map each retained unit to a destination page or section.
- Do not treat a broad "covered" row as proof for dense source material; use
line- or claim-level evidence when the source unit is dense.
- For dropped content, state whether it is obsolete, duplicated elsewhere,
unsupported, or moved to a reference/support page.
- When a docs-audit artifact is used, verify it is mapped audit data with
non-empty `mappings[]`, not only inventory or reindexed JSON.
## Validation
Choose the narrowest proof that covers the touched surface:
- `pnpm docs:list`
- `pnpm docs:check-mdx`
- `pnpm docs:check-links`
- `pnpm docs:check-i18n-glossary`
- `pnpm format:docs:check` or `pnpm lint:docs`
- `git diff --check`
- generated-doc or inventory checks when generated references, plugin catalogs,
labeler, or docs scripts changed
- behavior tests or command probes when docs claim runtime behavior
If proof is blocked, say exactly which command was not run and why.

View File

@@ -0,0 +1,54 @@
# Documentation Principles
This reference consolidates the core rules used by this skill.
## Matt Palmer: 8 rules for better docs
Source: https://mattpalmer.io/posts/2025/10/8-rules-for-better-docs/
Use these as default operating principles:
1. Write for humans, optimize for agents.
2. Start with a funnel: what/why, quickstart, next steps.
3. Use Diataxis to scaffold content.
4. Write with AI, but structure for agents.
5. Offload routine docs operations to background agents.
6. Automate quality with CI.
7. Automate scaffolding and repetitive workflow tasks.
8. Make contribution easy and visible.
## OpenAI cookbook: what makes documentation good
Source: https://cookbook.openai.com/articles/what_makes_documentation_good
Key quality constraints:
- Prefer specific and accurate terminology over niche jargon.
- Keep examples self-contained and minimize dependencies.
- Prioritize high-value topics over edge-case depth.
- Do not teach unsafe patterns (for example, exposed secrets).
- Open with context that helps readers orient quickly.
- Apply empathy and override rigid rules when it clearly improves outcomes.
## Practical merge policy
When these rules conflict:
1. Preserve reader task success first.
2. Preserve structural clarity second.
3. Preserve long-term maintainability third.
4. Add agent optimization only if it does not reduce human clarity.
For agent-instructions and contributor-governance specifics (AGENTS/aliases/CONTRIBUTING), use `references/agent-and-contributing.md` as the detailed additional source of truth.
When the target repo or request is OpenClaw-specific, layer `references/openclaw.md` on top of these general rules. Otherwise ignore that repo-specific overlay.
## Execution policy for this skill
- Long-running and extensive investigations are allowed for both build and review work when needed to resolve ambiguity or cross-file drift.
- Use sub-agents when available for bounded parallel discovery, verification, or cross-source comparison.
- Keep one merged outcome: sub-agent outputs must be normalized into a single consistent recommendation/fix set.
## Multilingual parity rule
When docs exist in multiple languages, target cross-locale parity for task-critical content (steps, warnings, prerequisites, and limits). If full parity is not possible, publish explicit parity status and sync intent.

View File

@@ -0,0 +1,121 @@
# Review Docs Playbook
Read `principles.md` first, then apply this checklist.
## 1. Scope and classification
- Identify doc type and target audience.
- Confirm brownfield vs evergreen intent.
- Confirm expected outcome for the reader.
- For full-repo reviews, explicitly include both governance surfaces and product-doc surfaces (`docs/`, README trees, `.md/.mdx/.mdc`, `.rst/.rsc`, framework docs configs).
- For OpenClaw docs reviews, apply `references/openclaw.md` for page type, docs IA, preservation, examples, and validation checks.
## 2. Investigation behavior
- Proactively find issues and risks without waiting for repeated prompts.
- If there are signals of deeper problems, continue investigation beyond the first pass.
- Long-running and extensive investigations are acceptable when needed for confidence and correctness.
- When available, use sub-agents for bounded parallel discovery (for example file-inventory, command validation, or cross-doc consistency checks), then merge to one final issue set.
- When no issues are found, state that explicitly and call out residual risks or validation gaps.
- Default to `apply-fixes` for high-confidence documentation defects unless the user explicitly requests `report-only`.
- Do not stop at AGENTS/CONTRIBUTING checks when the task is documentation-wide; continue into docs-content and docs-framework surfaces.
## 3. Governance surface review
- Use `references/agent-and-contributing.md` as the source of truth for inventory, canonical/alias mapping, and precedence/conflict handling.
For AGENTS.md:
- confirm persona intent, scope, and command/tool boundaries are explicit.
- check frontmatter style matches repo conventions when present.
- ensure `Always`, `Ask first`, and `Never` boundaries are present when expected.
- require concrete command examples and repo-specific paths to avoid ambiguity.
For CONTRIBUTING.md:
- verify issue/PR workflow is complete and actionable.
- ensure local setup, lint/test commands, and review criteria are accurate.
- ensure governance does not conflict with nested AGENTS instructions.
- flag oversized files that should be split into linked section docs (for example tool-specific setup and release docs).
For agent-platform awareness:
- confirm references are minimal and scoped for Cursor/Claude glob behavior.
- confirm Codex-facing guidance uses explicit file references.
- confirm both surfaces represent the same shared policy core (commands, boundaries, and precedence), not divergent guidance.
- audit `.agents`/`.cursor` compatibility behavior:
- verify canonical rule directory and symlink state match repo policy
- verify symlink target integrity and platform/tooling expectations
- verify AGENTS policy references remain canonical for Codex even when `.cursor` compatibility exists
- check for context bloat from duplicated policy statements across agent and contributor files.
- check for conflicting rules, skills and agent instructions
- check for conflicting information in agent instructions vs codebase
- check for broken or missing referenced files (for example README/index files named as canonical entry points).
- check for setup/command drift (for example non-existent install commands, root-level commands that should be module-scoped).
## 4. Product documentation surface review
- Verify docs IA coverage across root/module `README*` files and `docs/**` trees.
- Review framework-native docs sources in scope (for example Fern, Mintlify, Sphinx, MkDocs) and ensure guidance matches actual source-of-truth files.
- Check `.md/.mdx/.mdc/.rst/.rsc` for stale commands, missing prerequisites, and broken cross-links.
- Confirm referenced doc paths and anchors exist.
- Flag docs that should be split/merged to improve discoverability and maintenance.
- For OpenClaw docs, check `docs/docs.json`, docs-list routing hints, main path versus `Reference` placement, and generated-reference visibility.
- For OpenClaw rewrites or page splits, require source-backed keep/drop/move/destination coverage for important claims, warnings, examples, commands, fields, and troubleshooting facts.
## 5. Framework config and path mapping checks
- Detect and read framework config first (for example Fern config, Sphinx `conf.py`, Mintlify config, or equivalent).
- Resolve path references relative to the declaring file/config.
- Treat filesystem paths and published URL routes as separate maps; verify both.
- Flag path-map drift explicitly (`missing file`, `stale route`, `wrong base path`).
## 6. Structural review
- Funnel check: what/why, quickstart, next steps.
- Validate heading flow and navigation discoverability.
- Flag critical content trapped in images or buried sections.
- Check Diataxis alignment and split mixed-purpose sections.
- For OpenClaw docs, confirm the content matches an explicit page type from `references/openclaw.md`.
## 7. Writing quality review
- Check for concise, scannable paragraphs.
- Remove ambiguous pronouns and undefined terms.
- Verify examples are executable and scoped correctly.
- Verify tone is directive, technical, and non-hand-wavy.
## 8. Brownfield review mode
- Verify compatibility with existing docs IA and conventions.
- Verify anchors, redirects, and cross-doc links remain valid.
- Flag regressions in onboarding and task completion paths.
- Ensure changed terminology is intentionally propagated.
## 9. Evergreen review mode
- Flag date-stamped or brittle wording without version scope.
- Check ownership and refresh signals are present.
- Ensure recommendations remain valid after routine product evolution.
- Flag missing deprecation/migration guidance.
## 10. Tooling and platform review
Read `tooling.md` if platform fit is uncertain.
- Check whether content uses platform primitives effectively.
- Flag structure that fights the chosen docs platform.
- Recommend targeted platform-aware improvements.
## 11. Multilingual parity review (when applicable)
- Confirm declared source-of-truth language and expected parity policy.
- Compare changed sections across locales for step/order/warning drift.
- Flag missing updates to prerequisites, version notes, limits, and safety guidance.
- Allow intentional divergence only when rationale is explicit and user-impact is low.
- Require a reader-visible status note when locale parity is partial.
## 12. Output format
1. Blocking issues (file + required fix)
2. Non-blocking improvements
3. Validation notes (done vs pending)

View File

@@ -0,0 +1,32 @@
# Documentation Tooling Guide
Source: https://www.mintlify.com/blog/top-7-api-documentation-tools-of-2025
Use this file when deciding build/review expectations for doc platforms.
## Tool-selection checkpoints
- Existing stack lock-in: do not force migration for minor gains.
- API workflow depth: generated references, OpenAPI support, testability.
- Collaboration model: docs-as-code, review workflow, versioning.
- Runtime quality: search, navigation, and copy-ready code snippets.
- AI readiness: structured content, stable URLs, machine-friendly layout yet human readable.
- Human readiness: reading complexity, reading UX, navigation depth, minimize jargon.
## Apply in brownfield mode
- Prioritize compatibility with the current platform.
- Use available components and style conventions before introducing new patterns.
- Propose migration only when current constraints block critical outcomes.
## Apply in evergreen mode
- Favor platforms and templates that make routine updates low-friction.
- Standardize section templates to reduce drift.
- Capture ownership, update cadence, and stale-content detection rules.
## Review implications
- Check whether content uses platform primitives correctly (tabs, callouts, endpoint blocks).
- Flag docs that are technically correct but hard to scan in the chosen platform.
- Recommend platform-specific improvements only when they reduce cognitive load.

View File

@@ -123,14 +123,14 @@ runs:
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
bash scripts/ci-docker-pull-retry.sh "${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}"
bash scripts/ci-docker-pull-retry.sh "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
- name: Validate Docker E2E credentials
if: inputs.hydrate-artifacts == 'true'

View File

@@ -26,11 +26,23 @@ inputs:
runs:
using: composite
steps:
- name: Normalize container toolcache
shell: bash
run: |
set -euo pipefail
if [[ -d /__t && ! -e /opt/hostedtoolcache ]]; then
mkdir -p /opt
ln -s /__t /opt/hostedtoolcache
fi
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
check-latest: false
shell: bash
env:
REQUESTED_NODE_VERSION: ${{ inputs.node-version }}
run: |
set -euo pipefail
source "$GITHUB_ACTION_PATH/../setup-pnpm-store-cache/ensure-node.sh"
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
@@ -40,9 +52,10 @@ runs:
- name: Setup Bun
if: inputs.install-bun == 'true'
uses: oven-sh/setup-bun@v2.2.0
with:
bun-version: "1.3.13"
shell: bash
run: |
set -euo pipefail
npm install -g bun@1.3.13
- name: Runtime versions
shell: bash

View File

@@ -14,7 +14,7 @@ inputs:
required: false
default: ""
use-actions-cache:
description: Whether pnpm/action-setup should cache the pnpm store.
description: Whether actions/cache should cache the pnpm store.
required: false
default: "true"
outputs:
@@ -47,12 +47,42 @@ runs:
openclaw_ensure_node "$requested_node"
- name: Setup pnpm from packageManager
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
shell: bash
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}
run: |
set -euo pipefail
package_manager="$(node -e "const fs = require('node:fs'); const path = require('node:path'); const pkg = JSON.parse(fs.readFileSync(path.resolve(process.argv[1]), 'utf8')); process.stdout.write(pkg.packageManager || '')" "$PACKAGE_MANAGER_FILE")"
case "$package_manager" in
pnpm@*) ;;
*)
echo "::error::Expected packageManager to pin pnpm, got '${package_manager:-<empty>}'"
exit 1
;;
esac
corepack enable
corepack prepare "$package_manager" --activate
- name: Resolve pnpm store path
id: pnpm-store
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
shell: bash
run: |
set -euo pipefail
store_path="$(pnpm store path --silent)"
node -e "require('node:fs').mkdirSync(process.argv[1], { recursive: true })" "$store_path"
echo "path=$store_path" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
uses: actions/cache@v5
with:
package_json_file: ${{ inputs.package-manager-file }}
run_install: false
cache: ${{ inputs.use-actions-cache }}
cache_dependency_path: ${{ inputs.lockfile-path }}
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-${{ hashFiles(inputs.lockfile-path) }}
restore-keys: |
pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-
pnpm-store-${{ runner.os }}-
- name: Record pnpm version
id: pnpm-version

View File

@@ -28,9 +28,17 @@ openclaw_active_node_version() {
openclaw_prepend_node_bin() {
local node_bin_dir="$1"
export PATH="$node_bin_dir:$PATH"
local shell_node_bin_dir="$node_bin_dir"
if command -v cygpath >/dev/null 2>&1; then
shell_node_bin_dir="$(cygpath -u "$node_bin_dir" 2>/dev/null || printf '%s' "$node_bin_dir")"
fi
export PATH="$shell_node_bin_dir:$PATH"
if [[ -n "${GITHUB_PATH:-}" ]]; then
echo "$node_bin_dir" >> "$GITHUB_PATH"
local github_node_bin_dir="$shell_node_bin_dir"
if command -v cygpath >/dev/null 2>&1; then
github_node_bin_dir="$(cygpath -w "$shell_node_bin_dir" 2>/dev/null || printf '%s' "$shell_node_bin_dir")"
fi
echo "$github_node_bin_dir" >> "$GITHUB_PATH"
fi
hash -r
}
@@ -43,6 +51,7 @@ openclaw_find_toolcache_node() {
"${RUNNER_TOOL_CACHE:-}" \
"${AGENT_TOOLSDIRECTORY:-}" \
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
"${OPENCLAW_CONTAINER_TOOL_CACHE:-/__t}" \
"/opt/hostedtoolcache" \
"/home/runner/_work/_tool" \
"/Users/runner/hostedtoolcache" \
@@ -68,6 +77,56 @@ openclaw_find_toolcache_node() {
return 1
}
openclaw_resolve_node_download_version() {
local requested_node="$1"
if [[ "$requested_node" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
[[ "$requested_node" == v* ]] && printf '%s\n' "$requested_node" || printf 'v%s\n' "$requested_node"
return 0
fi
local prefix="${requested_node#v}"
prefix="${prefix%%[xX]*}"
prefix="v${prefix}"
[[ "$prefix" == *. ]] || prefix="${prefix}."
curl -fsSL https://nodejs.org/dist/index.json |
OPENCLAW_NODE_PREFIX="$prefix" python3 -c 'import json, os, sys
prefix = os.environ["OPENCLAW_NODE_PREFIX"]
for item in json.load(sys.stdin):
version = item.get("version", "")
if version.startswith(prefix):
print(version)
break
'
}
openclaw_node_download_platform() {
local os_name arch_name
os_name="$(uname -s)"
arch_name="$(uname -m)"
case "$os_name:$arch_name" in
Linux:x86_64) printf 'linux-x64\n' ;;
Linux:aarch64 | Linux:arm64) printf 'linux-arm64\n' ;;
Darwin:x86_64) printf 'darwin-x64\n' ;;
Darwin:arm64) printf 'darwin-arm64\n' ;;
*)
return 1
;;
esac
}
openclaw_download_node() {
local requested_node="$1"
local version platform archive_url install_root
version="$(openclaw_resolve_node_download_version "$requested_node")"
platform="$(openclaw_node_download_platform)" || return 1
install_root="${RUNNER_TEMP:-/tmp}/openclaw-node-${version}-${platform}"
archive_url="https://nodejs.org/dist/${version}/node-${version}-${platform}.tar.xz"
mkdir -p "$install_root"
echo "Downloading Node ${version} from ${archive_url}"
curl -fsSL "$archive_url" | tar -xJ -C "$install_root" --strip-components=1
openclaw_prepend_node_bin "$install_root/bin"
}
openclaw_ensure_node() {
local requested_node="${1:-}"
requested_node="${requested_node#v}"
@@ -86,6 +145,8 @@ openclaw_ensure_node() {
if [[ -n "$node_bin" ]]; then
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
openclaw_prepend_node_bin "$(dirname "$node_bin")"
else
openclaw_download_node "$requested_node" || true
fi
active_node_version="$(openclaw_active_node_version)"

View File

@@ -12,7 +12,7 @@ Hard limits:
- Do not change production code, tests, package metadata, generated baselines, lockfiles, or CI config.
- Keep changes minimal and factual.
- Use "plugin/plugins" in user-facing docs/UI/changelog; `extensions/` is only the internal workspace layout.
- Do not add a changelog entry unless the docs update describes a user-facing behavior/API change from the triggering commit.
- Do not add `CHANGELOG.md` entries during normal docs work. Capture user-facing release-note context in the PR body or commit message instead.
Allowed paths:

View File

@@ -61,7 +61,7 @@ jobs:
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" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \

View File

@@ -59,7 +59,7 @@ jobs:
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" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
@@ -132,6 +132,5 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
continue-on-error: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -76,13 +76,16 @@ jobs:
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Resolve checkout SHA
id: checkout_ref
@@ -299,13 +302,16 @@ jobs:
PRE_COMMIT_HOME: .cache/pre-commit-security-fast
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Ensure security base commit
if: github.event_name != 'workflow_dispatch'
@@ -335,22 +341,20 @@ jobs:
fi
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
- name: Setup Python
- name: Resolve Python runtime
id: setup-python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Restore pre-commit cache
uses: actions/cache@v5
with:
path: .cache/pre-commit-security-fast
key: pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-
run: |
set -euo pipefail
python3 --version
version="$(python3 - <<'PY'
import platform
print(platform.python_version())
PY
)"
echo "python-version=${version}" >> "$GITHUB_OUTPUT"
- name: Install pre-commit
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
run: python3 -m pip install --disable-pip-version-check pre-commit==4.2.0
- name: Detect committed private keys
run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files detect-private-key
@@ -383,10 +387,12 @@ jobs:
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24.x"
check-latest: false
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source .github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Audit production dependencies
run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
@@ -411,7 +417,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -427,10 +432,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -513,7 +518,24 @@ jobs:
run: pnpm test:build:singleton
- name: Check CLI startup memory
run: pnpm test:startup:memory
shell: bash
run: |
set +e
pnpm test:startup:memory
status=$?
if [[ -f .artifacts/startup-memory/summary.md ]]; then
cat .artifacts/startup-memory/summary.md >> "$GITHUB_STEP_SUMMARY"
fi
exit "$status"
- name: Upload startup memory report
if: always()
uses: actions/upload-artifact@v7
with:
name: startup-memory
path: .artifacts/startup-memory/
if-no-files-found: ignore
retention-days: 7
- name: Run built artifact checks
id: built_artifact_checks
@@ -619,7 +641,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -635,10 +656,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -706,7 +727,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -722,10 +742,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -787,7 +807,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -803,10 +822,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -865,7 +884,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -881,10 +899,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -941,7 +959,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -957,10 +974,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1064,7 +1081,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1080,10 +1096,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1195,7 +1211,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1211,10 +1226,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1345,7 +1360,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1361,10 +1375,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1391,12 +1405,13 @@ jobs:
install-bun: "false"
- name: Checkout ClawHub docs source
uses: actions/checkout@v6
with:
repository: openclaw/clawhub
path: clawhub-source
fetch-depth: 1
persist-credentials: true
run: |
set -euo pipefail
git init clawhub-source
git -C clawhub-source config gc.auto 0
git -C clawhub-source remote add origin "https://github.com/openclaw/clawhub.git"
git -C clawhub-source fetch --no-tags --depth=1 origin "+HEAD:refs/remotes/origin/checkout"
git -C clawhub-source checkout --detach refs/remotes/origin/checkout
- name: Check docs
env:
@@ -1412,11 +1427,16 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
@@ -1455,11 +1475,16 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Try to exclude workspace from Windows Defender (best-effort)
shell: pwsh
@@ -1481,10 +1506,12 @@ jobs:
}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24.x
check-latest: false
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source .github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
@@ -1548,11 +1575,16 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -1589,11 +1621,16 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Install XcodeGen / SwiftLint / SwiftFormat
run: brew install xcodegen swiftlint swiftformat
@@ -1693,7 +1730,6 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1709,10 +1745,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1

View File

@@ -141,7 +141,13 @@ jobs:
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl -fsSL https://get.docker.com | sudo sh
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
@@ -166,7 +172,12 @@ jobs:
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl -fsSL \
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"
@@ -307,7 +318,13 @@ jobs:
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl -fsSL https://get.docker.com | sudo sh
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
@@ -332,7 +349,12 @@ jobs:
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl -fsSL \
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"

View File

@@ -58,6 +58,7 @@ on:
- qa-parity
- qa-live
- npm-telegram
- performance
live_suite_filter:
description: Optional exact live/E2E suite id, or comma-separated QA live lanes such as qa-live-matrix,qa-live-telegram; blank runs all selected live suites
required: false
@@ -181,6 +182,11 @@ jobs:
else
echo "- Normal CI: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "performance" ]]; then
echo "- Product performance: \`OpenClaw Performance\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Product performance: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then
echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`"
else
@@ -239,7 +245,7 @@ jobs:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --foreground --kill-after=30s 35m docker build \
timeout --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
@@ -281,7 +287,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -411,7 +417,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -551,7 +557,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -853,7 +859,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -938,9 +944,127 @@ jobs:
exit 1
fi
performance:
name: Run product performance evidence
needs: [resolve_target, docker_runtime_assets_preflight]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
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 OpenClaw Performance
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
{
echo "### Product performance"
echo
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Profile: \`release\`"
echo "- Repeat: \`3\`"
echo "- Deep profile: \`false\`"
echo "- Live OpenAI candidate: \`false\`"
echo "- Release impact: advisory"
} >> "$GITHUB_STEP_SUMMARY"
before_json="$(gh_with_retry run list --workflow openclaw-performance.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
gh_with_retry workflow run openclaw-performance.yml \
--ref "$CHILD_WORKFLOW_REF" \
-f target_ref="$TARGET_SHA" \
-f profile=release \
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=false
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow openclaw-performance.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 "::warning::Could not find dispatched run for openclaw-performance.yml."
exit 0
fi
echo "Dispatched openclaw-performance.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow openclaw-performance.yml: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
poll_count=0
while true; do
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 10 == 0 )); then
echo "Still waiting on openclaw-performance.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh_with_retry run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh_with_retry run view "$run_id" --json url --jq '.url')"
echo "openclaw-performance.yml finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
echo "::warning::OpenClaw Performance is advisory and ended with ${conclusion}: ${url}"
gh_with_retry 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: [resolve_target, docker_runtime_assets_preflight, normal_ci, plugin_prerelease, release_checks, npm_telegram]
needs: [resolve_target, docker_runtime_assets_preflight, normal_ci, plugin_prerelease, release_checks, npm_telegram, performance]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
@@ -952,10 +1076,12 @@ jobs:
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}
NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}
PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
PERFORMANCE_RESULT: ${{ needs.performance.result }}
DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }}
RERUN_GROUP: ${{ inputs.rerun_group }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
@@ -963,6 +1089,29 @@ jobs:
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
@@ -1019,7 +1168,7 @@ jobs:
fi
local run_json status conclusion url attempt head_sha
run_json="$(gh run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs)"
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
url="$(jq -r '.url' <<< "$run_json")"
@@ -1066,7 +1215,7 @@ jobs:
fi
local run_json row
run_json="$(gh run view "$run_id" --json status,conclusion,url,createdAt,updatedAt,headSha)"
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,createdAt,updatedAt,headSha)"
row="$(
jq -r --arg label "$label" '
def ts: fromdateiso8601;
@@ -1088,6 +1237,7 @@ jobs:
append_child_row "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" "$PLUGIN_PRERELEASE_RESULT"
append_child_row "release_checks" "$RELEASE_CHECKS_RUN_ID" "$RELEASE_CHECKS_RESULT"
append_child_row "npm_telegram" "$NPM_TELEGRAM_RUN_ID" "$NPM_TELEGRAM_RESULT"
append_child_row "product_performance" "$PERFORMANCE_RUN_ID" "$PERFORMANCE_RESULT"
}
summarize_child_timing() {
@@ -1101,7 +1251,7 @@ jobs:
echo
echo "### Slowest jobs: ${label}"
echo
gh run view "$run_id" --json jobs --jq '
gh_with_retry run view "$run_id" --json jobs --jq '
def ts: fromdateiso8601;
"| Job | Result | Minutes |",
"| --- | --- | ---: |",
@@ -1118,7 +1268,7 @@ jobs:
echo
echo "### Longest queues: ${label}"
echo
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
def ts: fromdateiso8601;
"| Job | Result | Queue minutes | Run minutes |",
"| --- | --- | ---: | ---: |",
@@ -1147,7 +1297,7 @@ jobs:
fi
local run_json status conclusion artifacts_json
run_json="$(gh run view "$run_id" --json status,conclusion,url,jobs)"
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
@@ -1170,7 +1320,7 @@ jobs:
echo
echo "Artifacts:"
artifacts_json="$(
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
)"
if [[ -n "${artifacts_json// }" ]]; then
jq -r '
@@ -1246,6 +1396,7 @@ jobs:
summarize_child_timing "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
summarize_child_timing "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
summarize_child_timing "product_performance" "$PERFORMANCE_RUN_ID"
if [[ "$failed" != "0" ]]; then
summarize_failed_child "normal_ci" "$NORMAL_CI_RUN_ID"
@@ -1343,6 +1494,7 @@ jobs:
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}
run: |
set -euo pipefail
manifest_dir="${RUNNER_TEMP}/full-release-validation"
@@ -1361,6 +1513,7 @@ jobs:
--arg pluginPrereleaseRunId "$PLUGIN_PRERELEASE_RUN_ID" \
--arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \
--arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \
--arg performanceRunId "$PERFORMANCE_RUN_ID" \
'{
version: 1,
workflowName: $workflowName,
@@ -1376,7 +1529,8 @@ jobs:
normalCi: $normalCiRunId,
pluginPrerelease: $pluginPrereleaseRunId,
releaseChecks: $releaseChecksRunId,
npmTelegram: $npmTelegramRunId
npmTelegram: $npmTelegramRunId,
productPerformance: $performanceRunId
}
}' > "${manifest_dir}/full-release-validation-manifest.json"

View File

@@ -121,7 +121,7 @@ jobs:
# builder stalls; an explicit buildx invocation fails closed instead.
- name: Build root Dockerfile smoke image
run: |
timeout 45m docker buildx build \
timeout --kill-after=30s 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
@@ -132,7 +132,7 @@ jobs:
- name: Run root Dockerfile CLI smoke
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -163,7 +163,7 @@ jobs:
- name: Smoke test Dockerfile with matrix extension build arg
run: |
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -235,7 +235,7 @@ jobs:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
set -euo pipefail
if timeout 180s docker pull "$IMAGE_REF"; then
if timeout --kill-after=30s 180s docker pull "$IMAGE_REF"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Using existing root Dockerfile smoke image: \`$IMAGE_REF\`" >> "$GITHUB_STEP_SUMMARY"
else
@@ -256,7 +256,7 @@ jobs:
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
timeout 45m docker buildx build \
timeout --kill-after=30s 45m docker buildx build \
--progress=plain \
--push \
--build-arg OPENCLAW_EXTENSIONS=matrix \
@@ -320,13 +320,13 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
- name: Run root Dockerfile CLI smoke
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -359,7 +359,7 @@ jobs:
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -426,7 +426,7 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
@@ -435,7 +435,7 @@ jobs:
- name: Build installer smoke image
run: |
timeout 20m docker buildx build \
timeout --kill-after=30s 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-smoke:local \
@@ -444,7 +444,7 @@ jobs:
- name: Build installer non-root image
run: |
timeout 20m docker buildx build \
timeout --kill-after=30s 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-nonroot:local \
@@ -475,13 +475,22 @@ jobs:
- name: Run Rocky Linux installer smoke
run: |
timeout 20m docker run --rm \
timeout --kill-after=30s 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'
- name: Run Rocky Linux CLI installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install-cli.sh --prefix /tmp/openclaw-cli --version latest --no-onboard && /tmp/openclaw-cli/bin/openclaw --version'
bun_global_install_smoke:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
@@ -503,7 +512,7 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
- name: Setup Node environment for Bun smoke
uses: ./.github/actions/setup-node-env

View File

@@ -48,6 +48,7 @@ env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
CRABBOX_REF: main
CRABBOX_CAPACITY_REGIONS: eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2
MANTIS_OUTPUT_DIR: .artifacts/qa-e2e/mantis/telegram-desktop-proof
jobs:
@@ -422,7 +423,7 @@ jobs:
{
printf '%s\n' 'Defaults env_keep += "CODEX_HOME CODEX_INTERNAL_ORIGINATOR_OVERRIDE"'
printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"'
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER"'
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER CRABBOX_CAPACITY_REGIONS"'
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_CANDIDATE_TRUST MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_CREDENTIAL_OWNER_ID OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"'
@@ -451,6 +452,7 @@ jobs:
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
CRABBOX_CAPACITY_REGIONS: ${{ env.CRABBOX_CAPACITY_REGIONS }}
CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }}
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
GH_TOKEN: ${{ github.token }}
@@ -492,8 +494,11 @@ jobs:
exit 0
fi
status=0
mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/session.json' -type f -print0)
mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -name session.json -type f -print0)
for session_file in "${session_files[@]}"; do
if ! sudo -u codex node -e 'const fs = require("fs"); const session = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(session.command === "telegram-user-crabbox-session" ? 0 : 1);' "$session_file"; then
continue
fi
lease_file="${session_file%/session.json}/.session/lease.json"
if [[ ! -f "$lease_file" ]]; then
continue
@@ -508,8 +513,11 @@ jobs:
status=1
fi
done
mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/.session/lease.json' -type f -print0)
mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/.session/lease.json' -type f -print0)
for lease_file in "${lease_files[@]}"; do
if ! sudo -u codex node -e 'const fs = require("fs"); const lease = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(lease.kind === "telegram-user" ? 0 : 1);' "$lease_file"; then
continue
fi
if ! sudo -u codex env \
OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \
OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \

View File

@@ -553,6 +553,15 @@ jobs:
use-actions-cache: "false"
- name: Download candidate artifact
id: download_candidate
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
- name: Retry candidate artifact download
if: ${{ steps.download_candidate.outcome == 'failure' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
@@ -560,11 +569,38 @@ jobs:
- name: Download baseline artifact
if: ${{ matrix.suite == 'packaged-upgrade' }}
id: download_baseline
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Retry baseline artifact download
if: ${{ matrix.suite == 'packaged-upgrade' && steps.download_baseline.outcome == 'failure' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Verify release-check inputs
shell: bash
env:
CANDIDATE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}
BASELINE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}
SUITE: ${{ matrix.suite }}
run: |
mkdir -p "${OUTPUT_DIR}"
if [[ ! -f "${CANDIDATE_TGZ}" ]]; then
echo "::error::candidate artifact missing: ${CANDIDATE_TGZ}"
exit 1
fi
if [[ "${SUITE}" == "packaged-upgrade" ]] && [[ ! -f "${BASELINE_TGZ}" ]]; then
echo "::error::baseline artifact missing: ${BASELINE_TGZ}"
exit 1
fi
- name: Run cross-OS release checks
shell: bash
env:
@@ -615,7 +651,8 @@ jobs:
if [[ -f "${SUMMARY_PATH}" ]]; then
cat "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
else
echo "No summary generated." >> "$GITHUB_STEP_SUMMARY"
mkdir -p "$(dirname "${SUMMARY_PATH}")"
echo "No summary generated." | tee "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload release-check artifacts

View File

@@ -102,6 +102,11 @@ on:
- beta
- stable
- full
use_github_hosted_runners:
description: Use GitHub-hosted runners instead of Blacksmith runners
required: false
default: false
type: boolean
advisory:
description: Treat failures as advisory for the caller
required: false
@@ -208,6 +213,11 @@ on:
required: false
default: stable
type: string
use_github_hosted_runners:
description: Use GitHub-hosted runners instead of Blacksmith runners
required: false
default: true
type: boolean
secrets:
OPENAI_API_KEY:
required: false
@@ -474,7 +484,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: 20
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -511,7 +521,7 @@ jobs:
set -euo pipefail
for attempt in 1 2; do
echo "live-cache attempt ${attempt}/2"
if timeout --foreground --kill-after=30s 8m pnpm test:live:cache; then
if timeout --kill-after=30s 8m pnpm test:live:cache; then
exit 0
fi
if [[ "$attempt" == "2" ]]; then
@@ -524,7 +534,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
@@ -556,7 +566,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -630,7 +640,7 @@ jobs:
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -921,7 +931,7 @@ jobs:
needs: validate_selected_ref
if: inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
outputs:
groups_json: ${{ steps.groups.outputs.groups_json }}
@@ -950,7 +960,7 @@ jobs:
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes (${{ matrix.group.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1182,7 +1192,7 @@ jobs:
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (openwebui)
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -1308,7 +1318,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
permissions:
actions: read
@@ -1424,7 +1434,7 @@ jobs:
fi
echo "Validating Docker E2E package tarball: $target"
started_at="$(date +%s)"
timeout --foreground 5m node scripts/check-openclaw-package-tarball.mjs "$target"
timeout --kill-after=30s 5m node scripts/check-openclaw-package-tarball.mjs "$target"
finished_at="$(date +%s)"
echo "Docker E2E package tarball validation finished in $((finished_at - started_at))s."
digest="$(sha256sum "$target" | awk '{print $1}')"
@@ -1551,7 +1561,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-') || startsWith(inputs.live_suite_filter, 'docker-live-models'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
permissions:
contents: read
@@ -1624,7 +1634,7 @@ jobs:
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
strategy:
fail-fast: false
@@ -1768,14 +1778,14 @@ jobs:
- name: Run Docker live model sweep
if: contains(matrix.profiles, inputs.release_test_profile)
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
validate_live_models_docker_targeted:
name: Docker live models (selected providers)
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers != '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -1943,13 +1953,13 @@ jobs:
done
- name: Run Docker live model sweep
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
validate_live_provider_suites:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || (startsWith(inputs.live_suite_filter, 'native-live-') && !startsWith(inputs.live_suite_filter, 'native-live-extensions-media') && inputs.live_suite_filter != 'native-live-extensions-a-k'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -2251,6 +2261,7 @@ jobs:
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
bash .release-harness/scripts/ci-live-command-retry.sh
@@ -2270,7 +2281,7 @@ jobs:
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -2278,32 +2289,32 @@ jobs:
include:
- suite_id: live-gateway-docker
label: Docker live gateway OpenAI
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=300000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: beta minimum stable full
- suite_id: live-gateway-anthropic-docker
label: Docker live gateway Anthropic
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-sonnet-4-6 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-google-docker
label: Docker live gateway Google
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-advisory-docker-deepseek-fireworks
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory DeepSeek/Fireworks
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
@@ -2311,7 +2322,7 @@ jobs:
- suite_id: live-gateway-advisory-docker-opencode-openrouter
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory OpenCode/OpenRouter
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
@@ -2319,32 +2330,32 @@ jobs:
- suite_id: live-gateway-advisory-docker-xai-zai
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory xAI/Z.ai
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
profiles: full
- suite_id: live-cli-backend-docker
label: Docker live CLI backend
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh
timeout_minutes: 50
profile_env_only: false
profiles: stable full
- suite_id: live-acp-bind-docker
label: Docker live ACP bind
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh
timeout_minutes: 50
profile_env_only: false
profiles: stable full
- suite_id: live-codex-harness-docker
label: Docker live Codex harness
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-subagent-announce-docker
label: Docker live subagent announce
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh
timeout_minutes: 25
profile_env_only: false
profiles: stable full
@@ -2469,6 +2480,7 @@ jobs:
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
bash .release-harness/scripts/ci-live-command-retry.sh
@@ -2488,7 +2500,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'native-live-extensions-media') || inputs.live_suite_filter == 'native-live-extensions-a-k')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
container:
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
credentials:
@@ -2656,6 +2668,7 @@ jobs:
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-')))
env:
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
${{ matrix.command }}

View File

@@ -307,7 +307,36 @@ jobs:
exit 1
fi
report_md="${report_json%.json}.md"
effective_status="$status"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$status" != "0" ]]; then
if REPORT_JSON="$report_json" node <<'NODE'
const fs = require("node:fs");
const report = JSON.parse(fs.readFileSync(process.env.REPORT_JSON, "utf8"));
const statuses = report.summary?.statuses ?? {};
const nonPassStatuses = Object.entries(statuses)
.filter(([status, count]) => status !== "PASS" && Number(count) > 0);
const baselineRegressionCount =
Number(report.baseline?.comparison?.regressionCount ?? report.gate?.baseline?.regressionCount ?? 0);
const gate = report.gate;
const toleratedPartial =
gate?.verdict === "PARTIAL" &&
Number(gate.blockingCount ?? 0) === 0 &&
baselineRegressionCount === 0 &&
nonPassStatuses.length === 0;
if (!toleratedPartial) {
process.exit(1);
}
NODE
then
effective_status=0
{
echo "Kova returned a partial release-gate verdict for filtered performance coverage, but all selected scenarios passed and no baseline regression was reported."
echo
} >> "$GITHUB_STEP_SUMMARY"
fi
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
echo "effective_status=$effective_status" >> "$GITHUB_OUTPUT"
echo "report_json=$report_json" >> "$GITHUB_OUTPUT"
echo "report_md=$report_md" >> "$GITHUB_OUTPUT"
@@ -344,8 +373,43 @@ jobs:
EOF
cat "$summary_path" >> "$GITHUB_STEP_SUMMARY"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$status" != "0" ]]; then
exit "$status"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$effective_status" != "0" ]]; then
exit "$effective_status"
fi
- name: Fetch previous source performance baseline
if: ${{ steps.lane.outputs.run == 'true' && matrix.lane == 'mock-provider' && steps.clawgrit.outputs.present == 'true' }}
env:
CLAWGRIT_REPORTS_TOKEN: ${{ secrets.CLAWGRIT_REPORTS_TOKEN }}
shell: bash
run: |
set -euo pipefail
reports_root=".artifacts/clawgrit-baseline"
mkdir -p "$reports_root"
git -C "$reports_root" init -b main
git -C "$reports_root" remote add origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
if ! git -C "$reports_root" fetch --depth=1 origin main; then
echo "No previous source performance baseline could be fetched." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git -C "$reports_root" checkout -B main FETCH_HEAD
ref_slug="$(printf '%s' "${TESTED_REF}" | tr -c 'A-Za-z0-9._-' '-')"
pointer="${reports_root}/openclaw-performance/${ref_slug}/latest-mock-provider.json"
if [[ ! -f "$pointer" ]]; then
echo "No previous source performance baseline exists for ${TESTED_REF}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if ! latest_path="$(node -e "const fs=require('node:fs'); const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); const value=String(data.path || ''); if (!/^openclaw-performance\\/[A-Za-z0-9._-]+\\/[0-9]+-[0-9]+\\/mock-provider$/u.test(value)) process.exit(1); process.stdout.write(value);" "$pointer")"; then
echo "Previous source performance baseline pointer is invalid." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
baseline_source="${reports_root}/${latest_path}/source"
if [[ -d "$baseline_source" ]]; then
baseline_source="$(realpath "$baseline_source")"
echo "SOURCE_PERF_BASELINE_DIR=$baseline_source" >> "$GITHUB_ENV"
echo "Using source performance baseline: ${latest_path}/source" >> "$GITHUB_STEP_SUMMARY"
else
echo "Previous source performance baseline has no source directory." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Run OpenClaw source performance probes
@@ -359,7 +423,7 @@ jobs:
fi
mkdir -p "$SOURCE_PERF_DIR/mock-hello"
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts.openclaw && fs.existsSync('scripts/bench-cli-startup.ts') ? 0 : 1)"; then
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts['test:extensions:memory'] && scripts.openclaw && fs.existsSync('scripts/bench-cli-startup.ts') && fs.existsSync('scripts/profile-extension-memory.mjs') ? 0 : 1)"; then
cat > "$SOURCE_PERF_DIR/index.md" <<EOF
# OpenClaw Source Performance
@@ -371,7 +435,7 @@ jobs:
- Tested ref: ${TESTED_REF}
- Tested SHA: ${TESTED_SHA}
- Required scripts: test:gateway:cpu-scenarios, openclaw, scripts/bench-cli-startup.ts
- Required scripts: test:gateway:cpu-scenarios, test:extensions:memory, openclaw, scripts/bench-cli-startup.ts, scripts/profile-extension-memory.mjs
EOF
cat "$SOURCE_PERF_DIR/index.md" >> "$GITHUB_STEP_SUMMARY"
exit 0
@@ -391,6 +455,9 @@ jobs:
--startup-case fiftyPlugins \
--startup-case fiftyStartupLazyPlugins
pnpm test:extensions:memory \
-- --json "$SOURCE_PERF_DIR/extension-memory.json"
for run_index in $(seq 1 "$source_runs"); do
run_dir="$SOURCE_PERF_DIR/mock-hello/run-$(printf '%03d' "$run_index")"
pnpm openclaw qa suite \
@@ -460,9 +527,13 @@ jobs:
cleanup_gateway
trap - EXIT
node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
--source-dir "$SOURCE_PERF_DIR" \
--output "$SOURCE_PERF_DIR/index.md"
--output "$SOURCE_PERF_DIR/index.md")
if [[ -n "${SOURCE_PERF_BASELINE_DIR:-}" && -d "$SOURCE_PERF_BASELINE_DIR" ]]; then
summary_args+=(--baseline-source-dir "$SOURCE_PERF_BASELINE_DIR")
fi
"${summary_args[@]}"
cat "$SOURCE_PERF_DIR/index.md" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -344,7 +344,7 @@ jobs:
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
OPENCLAW_VITEST_MAX_WORKERS: 1
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
run: pnpm test:extensions:batch "$OPENCLAW_EXTENSION_BATCH" -- --exclude extensions/codex/src/app-server/run-attempt.test.ts
plugin-prerelease-inspector:
permissions:

View File

@@ -42,7 +42,7 @@ jobs:
run: |
set -euo pipefail
docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF'
timeout --kill-after=30s 5m docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF'
FROM debian:bookworm-slim
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox
@@ -63,5 +63,5 @@ jobs:
FINAL_USER=sandbox \
scripts/sandbox-common-setup.sh
u="$(docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
u="$(timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
test "$u" = "sandbox"

View File

@@ -38,4 +38,4 @@ jobs:
install-bun: "false"
- name: Run TUI PTY tests
run: timeout 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

View File

@@ -75,14 +75,14 @@ jobs:
- name: install.sh in Docker
run: |
docker run --rm \
timeout --kill-after=30s 20m docker run --rm \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
node:24-bookworm-slim \
bash -lc 'bash /tmp/install.sh --version latest && openclaw --version'
- name: install-cli.sh in Docker
run: |
docker run --rm \
timeout --kill-after=30s 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \

View File

@@ -26,7 +26,16 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Fail on tabs in workflow files
run: |
@@ -58,7 +67,16 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Install actionlint
shell: bash
@@ -90,7 +108,16 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment
uses: ./.github/actions/setup-node-env

5
.gitignore vendored
View File

@@ -128,7 +128,8 @@ mantis/
!.agents/skills/control-ui-e2e/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/openclaw-docs/**
!.agents/skills/technical-documentation/
!.agents/skills/technical-documentation/**
!.agents/skills/openclaw-refactor-docs/
!.agents/skills/openclaw-refactor-docs/**
!.agents/skills/openclaw-debugging/
@@ -167,6 +168,8 @@ mantis/
!.agents/skills/tag-duplicate-prs-issues/**
!.agents/skills/autoreview/
!.agents/skills/autoreview/**
.agents/skills/**/__pycache__/
.agents/skills/**/*.py[cod]
# Agent credentials and memory (NEVER COMMIT)
/memory/

View File

@@ -17,6 +17,22 @@ Skills own workflows; root owns hard policy and routing.
- New channel/plugin/app/doc surface: update `.github/labeler.yml` + GH labels.
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink; edit `AGENTS.md` only.
## ClawSweeper Review Policy
- OpenClaw-specific review rules live here; generic ClawSweeper prompts stay repo-agnostic.
- ClawSweeper-owned schema, labels, close reasons, protected-label gates, maintainer-item gates, and mutation rules live in `openclaw/clawsweeper`.
- Review workers read this full root `AGENTS.md` before judging; no reliance on search snippets, `head`, partial ranges, local excerpts, or truncated copies. Then read every scoped `AGENTS.md` that owns touched paths.
- Optional integrations, providers, channels, skill bundles, MCP surfaces, and service workflows route to plugins, ClawHub, or owner repos when current seams suffice. Keep core items for missing core/plugin APIs, bundled regressions, security/core hardening, or maintainer product decisions.
- Plugin APIs, provider routing, auth/session state, persisted preferences, config loading, config/default additions, migrations, setup, startup checks, and fallback behavior are compatibility/upgrade-sensitive. Treat config breaks, new config/default surfaces, removed fallbacks, fail-closed changes, stricter validation, or new operator action as merge risk even with green CI when they can affect existing users, upgrades, provider/plugin behavior, or maintainer operations.
- For PRs that add, remove, or change config/default surfaces with possible compatibility, upgrade, provider/plugin, operator, setup, startup, or fallback impact, ClawSweeper review should emit a `reviewMetrics` entry when practical. The metric should name the count and direction of the changes, such as added, changed, or removed config/default surfaces, and explain why the metric matters before merge. When the metric indicates concrete merge risk, also surface the concern in `risks`, use `mergeRiskLabels` when the risk matches the label rubric, make `bestSolution` name the desired pre-merge state, and ensure `labelJustifications` explain the specific reason rather than restating the label.
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
- Changelog findings: see Docs / Changelog.
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; real behavior proof matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
- Prefer findings for concrete behavior regressions, missing changed-surface proof, owner-boundary violations, security/API contract issues, or docs/config mismatches.
- Do not file findings for repo policy preference when changed code follows the relevant scoped guide and no user-visible, runtime, security, or maintainer-risk impact is shown.
## Map
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`.
@@ -40,11 +56,19 @@ Skills own workflows; root owns hard policy and routing.
- Internal bundled plugins ship in core dist; bundled-only facade loader ok only for them.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Legacy config repair belongs in `openclaw doctor --fix`, not startup/load-time core migrations. Runtime paths use canonical contracts.
- Runtime reads canonical config only. No silent compat for old/malformed config keys. If a config change invalidates existing files, add a matching `openclaw doctor --fix` migration. Core/auth config repairs live in core doctor; plugin-owned config repairs live in that plugin's doctor contract (`legacyConfigRules` / `normalizeCompatibilityConfig`).
- Fix shape: default to clean bounded refactor, not smallest patch. Move ownership to right boundary; delete stale abstractions, duplicate policy, dead branches, wrappers, fallback stacks.
- Fix observed local failures with generic product rules; do not hardcode names, ids, log phrases, or user examples in prod code unless they are an explicit contract.
- Tests may use observed examples, but prod literals need a short contract reason.
- Compatibility is opt-in. "Shipped" means reachable from a release Git tag; main/GitHub/PR/unreleased code is not shipped.
- Refactor default: one canonical path. Delete the old path unless user explicitly wants compat or the shipped public contract is obvious and cited.
- Keep old behavior only for an explicit public API/config/plugin SDK/data contract, tagged upgrade path, security/migration boundary, dependency contract, or observed prod state.
- If unsure, ask before preserving compat. Do not keep aliases, shims, fallback stacks, stale names, or obsolete tests just in case.
- Tests alone do not make internals contracts. If compat stays, name the contract and migration/removal plan in code, test, or PR.
- Lean code is a goal. No internal shims, aliases, legacy names, broad fallbacks, or defensive branches just to reduce diff or handle unrealistic edge cases.
- Handle real production states, shipped upgrade paths, security boundaries, and dependency contracts. Public/hostile/observed malformed input gets care; hypothetical malformed input does not.
- Public plugin SDK/API is the compat exception. New API first, old path only via named compat/deprecation metadata, docs, warnings when useful, tests for old+new, planned removal.
- Handle real production states, tagged upgrade paths, security boundaries, and dependency contracts. Public/hostile/observed malformed input gets care; hypothetical malformed input does not.
- Deprecate shipped public contracts only.
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
@@ -52,7 +76,8 @@ Skills own workflows; root owns hard policy and routing.
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
- Runtime hot paths: no freshness polling (`stat`/`realpath`/JSON reread/hash). Reuse current snapshots, install records, discovery, lookup tables, root scopes, resolved paths.
- Process-local metadata caches ok when lifecycle-owned and bounded/single-slot. Freshness exceptions need named owner + tests.
- Inline code comments: brief notes for tricky, bug-prone, or previously buggy logic.
- Inline comments: preserve reviewer context at the code site. Use for cross-path/state invariants, platform/dependency caps, deterministic ordering, compact encoded state, lifecycle ordering, ownership boundaries, session/id adoption, queue-depth symmetry, fallbacks, or intentional caller differences.
- Comment shape: 1-3 short lines; state why the branch/helper exists, what contract it protects, and the bad outcome if removed. Cite nearby constants/helpers when useful. No syntax narration, PR/user-specific lore, or obvious mechanics.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Protocol version bumps: explicit owner confirmation only; never automatic/generated.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor only.
@@ -64,7 +89,6 @@ Skills own workflows; root owns hard policy and routing.
- Runtime: Node 22.19+; Node 24 recommended. Keep Node + Bun paths working.
- Package manager/runtime: repo defaults only. No swaps without approval.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- Sharp/Homebrew libvips source-build fail: `SHARP_IGNORE_GLOBAL_LIBVIPS=1 pnpm install`.
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
@@ -101,7 +125,6 @@ Skills own workflows; root owns hard policy and routing.
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
- Issue/PR final answer: last line is the full GitHub URL.
- Changelog: PR landings/fixes need one unless pure test/internal. Do not mention missing changelog as a review finding; Codex handles it during fix/landing.
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
@@ -109,8 +132,10 @@ Skills own workflows; root owns hard policy and routing.
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Do not commit `.github/pr-assets`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- Maintainers: may skip/ignore `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
@@ -125,10 +150,22 @@ Skills own workflows; root owns hard policy and routing.
- Calls should be boring: complex decisions happen above; call args/object fields are names, literals, or simple property reads.
- Prefer early returns over nested condition pyramids. Split code into gather -> normalize -> decide -> act.
- Use named intermediates only for domain meaning or readability; avoid temp-variable soup.
- Code size matters. Prefer small clear code; maintainability includes not growing LOC without payoff.
- Refactors should delete about as much local complexity as they add. If LOC grows, the new ownership/API needs to clearly pay for it.
- Before adding helpers/files, check whether existing code can absorb the behavior with less new surface.
- Keep APIs narrow: export only current caller needs; keep types/helpers local by default.
- Return the smallest useful shape. Avoid broad result objects, flags, metadata unless callers use them.
- Avoid adapter layers that only rename fields. Move real responsibility or leave code local.
- Inline simple one-use objects/spreads when clearer. Extract only when it removes duplication or hard logic.
- Tests prove behavior/regressions, not every internal branch.
- For non-trivial refactors, check `git diff --numstat` before closeout. If LOC grew, trim or explain why.
- Prefer existing narrow helpers over repeated casts/guards. Add local helpers when 2+ nearby call sites share real boundary logic.
- Prefer ctor parameter properties for injected deps/config. Do not ban them for erasable-syntax purity.
- Prefer `satisfies` for registries/config maps; derive types from schemas when a runtime schema already exists.
- Table-drive repetitive tests when it reduces code and keeps failure names clear.
- Dynamic import: no static+dynamic import for same prod module. Use `*.runtime.ts` lazy boundary. After edits: `pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` + architecture/madge green.
- Classes: no prototype mixins/mutations. Prefer inheritance/composition. Tests prefer per-instance stubs.
- Comments: brief, only non-obvious logic.
- Split files around ~700 LOC when clarity/testability improves.
- Naming: **OpenClaw** product/docs; `openclaw` CLI/package/path/config.
- English: American spelling.
@@ -147,12 +184,12 @@ Skills own workflows; root owns hard policy and routing.
## Docs / Changelog
- Use `$openclaw-docs` for docs writing/review. Docs change with behavior/API.
- Use `$technical-documentation` for docs writing/review. Docs change with behavior/API.
- Codex harness upgrade (`extensions/codex/package.json` `@openai/codex`): refresh `docs/plugins/codex-harness.md` model snapshot from the new harness `model/list`.
- Docs final answers: include relevant full `https://docs.openclaw.ai/...` URL(s). If issue/PR work too, GitHub URL last.
- Changelog entries: active version `### Changes`/`### Fixes`; single-line bullets only.
- Contributor PR authors should not edit `CHANGELOG.md`; maintainer/AI adds entries during landing/merge.
- Contributor-facing changelog entries thank credited human `@author`. Never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`; if unknown, omit thanks.
- `CHANGELOG.md`: release-owned. Do not edit for normal PRs, direct `main` fixes, or `ship it`; only explicit release/changelog generation may rewrite it. Do not ask contributors/agents for changelog edits.
- User-facing `fix`/`feat`/`perf`: put release-note context in PR body, squash message, or direct commit: behavior, surface, issue/PR refs, credited human author/reporter.
- Release generation: derive `CHANGELOG.md` from merged PRs + all direct `main` commits. Entries: active `### Changes`/`### Fixes`, single-line, thank credited humans; never thank bots/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
## Git
@@ -161,7 +198,7 @@ Skills own workflows; root owns hard policy and routing.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- User says `ship it`: changelog if needed, commit intended changes, pull --rebase, push.
- User says `ship it`: commit intended changes, pull --rebase, push.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.
@@ -172,7 +209,7 @@ Skills own workflows; root owns hard policy and routing.
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
- Lockfiles/shrinkwrap are security surface: review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `package-lock.json`; root/plugin npm packages ship shrinkwrap, not package-lock.
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps need explicit approval. Use `$openclaw-release-maintainer`.
- Releases/publish/version bumps need explicit approval. Use `$release-openclaw-maintainer`.
- GHSA/advisories: `$openclaw-ghsa-maintainer` / `$security-triage`. Secret scanning: `$openclaw-secret-scanning-maintainer`.
- Beta tag/version match: `vYYYY.M.D-beta.N` -> npm `YYYY.M.D-beta.N --tag beta`.
@@ -183,7 +220,7 @@ Skills own workflows; root owns hard policy and routing.
- SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
- Mac gateway: dev watch = `pnpm gateway:watch`; managed installs = `openclaw gateway restart/status --deep`; logs = `./scripts/clawlog.sh`. No launchd/ad-hoc tmux.
- Mac app permission testing: stable app path + real signing identity required. No `--no-sign`, `SIGN_IDENTITY=-`, or raw debug binary; TCC prompts/listing won't stick.
- Version bump surfaces live in `$openclaw-release-maintainer`.
- Version bump surfaces live in `$release-openclaw-maintainer`.
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.

View File

@@ -6,10 +6,181 @@ Docs: https://docs.openclaw.ai
### Changes
- Voice: expose shared realtime turn-context tracking through the realtime voice SDK and reuse it for Discord speaker attribution and wake-name context recovery.
- Voice: reuse shared realtime output activity tracking in Google Meet command and node audio bridges, including recent-output checks for local barge-in detection.
- Voice: expose shared realtime output activity tracking through the realtime voice SDK and reuse it for Discord playback activity and barge-in decisions.
- Voice: expose shared realtime consult question matching, speakable-result extraction, and alias-aware forced-consult coordination through the realtime voice SDK, then reuse it in Gateway Talk, Voice Call, and Discord voice paths.
- Voice: share activation-name matching and consult-transcript screening through the realtime voice SDK so Discord, browser voice, and meeting surfaces can reuse one implementation.
- Cron: default `cron.maxConcurrentRuns` to 8 so scheduled automations and their isolated agent turns can make progress in parallel without explicit configuration.
- QA-Lab: add `qa coverage --match <query>` so focused proof selection can discover matching scenarios from existing metadata before running live or remote lanes.
- Control UI: add an ephemeral Activity tab for sanitized live tool activity summaries without persisting raw telemetry. Fixes #12831. Thanks @BunsDev.
- Build: include `ui:build` in the `full` and `ciArtifacts` profiles of `scripts/build-all.mjs` so `pnpm build` always rebuilds `dist/control-ui` after `tsdown` cleans `dist`, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)
- Migrate: import supported Hermes, OpenCode, and Codex auth credentials into OpenClaw auth profiles when credential migration is selected, with explicit opt-out and non-interactive controls. (#85667) Thanks @fuller-stack-dev.
- iOS: improve Talk mode with direct realtime voice sessions, compact toolbar status, and responsive voice waveform feedback. (#86355) Thanks @ngutman.
- Media: replace the Sharp image backend with Photon for metadata, resizing, EXIF orientation, and PNG alpha-preserving optimization so OpenClaw no longer installs Sharp or the WhatsApp Jimp fallback for image processing. (#86437)
### Fixes
- Telegram/network: treat `ENETDOWN` as a transient pre-connect network failure so Telegram sends, gateway unhandled-rejection handling, and cron network retries follow the same recovery path as sibling network outages. (#86762) Thanks @TurboTheTurtle.
- Agents/sessions: include visibility metadata on restricted `sessions_list` results so scoped counts are clearly reported without widening access or exposing hidden-session counts. (#86944) Thanks @ferminquant.
- Gateway/DNS: validate wide-area discovery domains before deriving zone paths or writing zone files, so invalid `discovery.wideArea.domain` and `dns setup --domain` values fail with a DNS-name diagnostic instead of falling through to unrelated configuration errors. Thanks @mmaps.
- Agents/BTW: route fallback side-question streams through the embedded stream resolver so Anthropic-compatible MiniMax requests use the same capped transport as normal chat. (#86312) Thanks @neeravmakwana.
- Telegram: treat `/command@TargetBot` bot-command entities as explicit mentions for the addressed bot so `requireMention` groups no longer drop targeted commands or captions. Fixes #84462. (#86553) Thanks @luoyanglang.
- CI: bound Docker/Bash E2E tarball npm installs with `OPENCLAW_E2E_NPM_INSTALL_TIMEOUT` so package, onboarding, plugin, and upgrade lanes fail instead of hanging on a stuck npm install.
- CI: keep `OPENCLAW_TESTBOX=1 pnpm check:changed` delegating to Blacksmith Testbox through Crabbox without forwarding local Testbox or worker env into the remote command.
- CI: send KILL after the TERM grace period for manual checkout fetch timeouts so stuck Testbox and workflow checkout retries cannot hang behind a wedged `git fetch`.
- iMessage: thread current channel/account inbound attachment roots into the image tool so iMessage-saved attachments under `~/Library/Messages/Attachments` (including the wildcard `/Users/*/Library/Messages/Attachments` root) are read through the existing inbound path policy instead of being rejected as `path-not-allowed`. Literal `localRoots` stays workspace-scoped. Fixes #30170. (#86569)
- QQ Bot: respect `OPENCLAW_HOME` for outbound media path resolution so `<qqmedia>` sends no longer silently fail when `HOME` and `OPENCLAW_HOME` differ (Docker / multi-user hosts). Persisted QQ Bot data (sessions, known users, refs) stays anchored on the OS home for upgrade compatibility. Fixes #83562. Thanks @sliverp.
- Update: report the primary malformed `openclaw.extensions` payload error without adding a duplicate missing-main diagnostic. (#86596) Thanks @ferminquant.
- Control UI: keep host-local Markdown file paths inert while preserving app-relative links. (#86620) Thanks @BryanTegomoh.
- Gateway: dampen repeated unauthenticated device-required probes per URL while preserving explicit-auth and paired recovery paths. (#86575) Thanks @ferminquant.
- IRC: store inbound channel routes with the canonical `channel:#name` target and join transient channel sends before writing. (#85906) Thanks @Kailigithub.
- Usage: surface unknown all-zero model pricing as missing cost entries instead of a confident `$0` total. (#85882) Thanks @MichaelZelbel.
- Agents/Codex: honor yolo app-server approval policy only for the full `never` plus `danger-full-access` case. (#85909) Thanks @earlvanze.
- Gateway/Gmail: clear Gmail watcher renewal intervals on re-entry so hot reloads do not leak lifecycle timers. (#82947) Thanks @SebTardif.
- Logging: exit cleanly on broken stdout/stderr pipes without masking existing failure exit codes. (#80059) Thanks @pavelzak.
- Gateway/security: escape transcript metadata field names while extracting oversized session line prefixes. (#85934) Thanks @SebTardif.
- Plugins/security: validate manifest model pattern regexes with the safe-regex compiler so unsafe patterns are ignored before matching. (#86046) Thanks @SebTardif.
- Discord: route gateway metadata REST lookups through the configured Discord proxy so proxied accounts do not fall back to direct `discord.com` connections before opening the WebSocket. Fixes #80227. Thanks @Clivilwalker.
- Agents/media: hydrate current-turn image attachments from filename-derived MIME types so active vision can see generated or forwarded images whose source omitted an image content type. (#84812) Thanks @marchpure.
- Agents/fs: point workspace-only scratch-path guidance at in-workspace temp directories while keeping host-root writes rejected by the tool guard. (#86501) Thanks @tianxiaochannel-oss88.
- Agents/media: keep async cron media completions scoped to their run session while preserving direct delivery for stale generated-media success and failure notifications. (#86529) Thanks @ai-hpc.
- Gateway: emit plugin `session_end`/`session_start` hooks when `agent.send` rotates or replaces a session id, keeping hook lifecycle state aligned with `sessions.changed` notifications. Fixes #83507. (#85875) Thanks @brokemac79.
- OpenShell/SSH: reject malformed generated exec commands before sandbox/session setup so unresolved workflow placeholders fail fast instead of reaching the remote shell. Fixes #72373. Thanks @brokemac79.
- Google: stop normalizing `gemini-3.1-flash-lite` to the retired preview endpoint and update Flash Lite alias guidance to the GA model id. Fixes #86151. (#86240) Thanks @SebTardif.
- Installer: make Alpine apk installs cover Git, verify the Node runtime floor, try `nodejs-current`, and report Alpine version guidance when repositories only provide older Node packages.
- Agents/status: prefer the active Claude CLI OAuth auth label over an unused Anthropic env API-key label for equivalent runtime aliases. Fixes #80184. (#86570) Thanks @brokemac79.
- Agents/media: send direct fallback for generated media still missing after an active requester wake fails. (#85489) Thanks @fuller-stack-dev.
- Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.
- Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.
- Agents/Codex: allow Codex app-server runs to bootstrap from `CODEX_API_KEY` or `OPENAI_API_KEY` when no Codex auth profile is configured.
- Agents/Codex: keep selected Codex runtime routing on OpenAI-Codex while preserving direct OpenAI API-key compaction fallback. (#86408) Thanks @funmerlin and @VACInc.
- Agent transcript: include OpenClaw agent session logs when finding local transcript candidates.
- Crabbox: bootstrap raw AWS macOS shell commands wrapped in absolute `time` paths so RSS probes can run Node and pnpm on fresh macOS runners.
- Crabbox: bootstrap raw AWS macOS shell commands even when setup statements precede Node or pnpm usage.
- TUI/local: skip unnecessary secret resolution, gateway model catalog loading, bootstrap, and skill scans in explicit local-model runs so startup reaches the model request faster.
- Sessions/doctor: load large session stores without clone amplification during read-only doctor checks and reclaim stale `sessions.json.*.tmp` sidecars. Fixes #56827. Thanks @openperf.
- Tests: clean successful plugin gateway gauntlet isolated temp roots while keeping an explicit preservation switch for failed/debug runs.
- Plugins/perf: reuse derived plugin metadata snapshots for the lifetime of the process so reply-time skill setup no longer rescans plugin metadata on every turn.
- Discord/OpenAI voice: keep wake-name master consults using the current speaker context after ignored ambient transcripts and shorten the default capture silence grace.
- Doctor: skip redundant Gateway restart prompts when a recent supervisor restart leaves the Gateway healthy. Fixes #86518. (#86533) Thanks @liaoyl830.
- Cron: restore suspended cron lanes to the configured/default concurrency instead of falling back to one after quota or circuit-breaker auto-resume.
- Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.
- Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run `ENOENT` failures. Fixes #82928. Thanks @galiniliev.
- Discord: merge streamed text captions into following media block replies so captions and attachments send as one message. (#86487) Thanks @neeravmakwana.
- Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.
- Discord/OpenAI voice: accept broader edge-position fuzzy wake-name transcripts while keeping ambient speech gated.
- Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.
- Agents/OpenAI-compatible: stop ModelStudio-compatible chat requests before sending system/tool-only payloads that have no usable user or assistant turn. (#86177) Thanks @TurboTheTurtle.
- Gateway/plugins: reuse plugin package realpath checks while building installed plugin indexes so startup avoids repeated filesystem resolution work.
- Kilo Gateway: send string `stop` sequences as arrays so Kilo accepts OpenAI-compatible chat completions. (#86461) Thanks @SebTardif.
- Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.
- Media understanding: convert HEIC and HEIF images to JPEG before image description providers run so iPhone photos work in direct and configured image-description flows. (#86037)
- Agents: release embedded-attempt session locks from outer teardown so post-prompt exceptions cannot wedge later requests behind `SessionWriteLockTimeoutError`. Fixes #86014. Thanks @openperf.
- Discord/OpenAI voice: rotate Realtime sessions at provider max duration without logging the expected session-expiry event as an error.
- Sessions: skip metadata-only entries during QMD-slugified session lookup so one incomplete row does not block transcript hit resolution. (#86327) Thanks @abnershang.
- Agents/media: derive bundled plugin local-media trust from plugin tool metadata instead of importing the full plugin registry on subscription paths. (#84409) Thanks @samzong.
- Image tool: keep config-backed custom-provider API keys usable for auto-discovered vision models, including deferred image-tool execution without env keys or auth profiles. (#85733)
- Memory/local embeddings: run local GGUF embeddings in an isolated worker sidecar and degrade to configured fallback or keyword search on worker failure so native embedding crashes do not take down the Gateway. (#85348) Thanks @osolmaz.
- Gateway: clear the runtime config snapshot before `SIGUSR1` in-process restarts so config changes survive the next gateway loop. (#86388) Thanks @XuZehan-iCenter.
- Models: show OAuth delegation markers as configured `models.json` auth while keeping runtime route usability checks strict. (#86378) Thanks @rohitjavvadi.
- Cron: seed active scheduled and manual cron task rows with a progress summary so status surfaces do not look blank while jobs run. (#86313) Thanks @ferminquant.
- Cron: preserve unsupported persisted cron payload rows during routine store writes while keeping those rows non-runnable. Fixes #84922. (#86415) Thanks @IWhatsskill.
- Updater: exclude prerelease git tags from stable channel resolution so source updates do not check out newer alpha/rc/preview/canary tags. (#86260) Thanks @stevenepalmer.
- Security/Audit: flag webhook `hooks.token` reuse of active Gateway password auth in `openclaw security audit` while keeping password-mode startup compatibility. (#84338) Thanks @coygeek.
- QQBot: derive the outbound reply watchdog from configured agent and provider timeouts so slow local model replies are not cut off at five minutes. Fixes #85267. (#85271) Thanks @SymbolStar.
- Agents/heartbeat: stop heartbeat turns after the first valid `heartbeat_respond` so repeated response loops do not burn tokens. (#86357) Thanks @udaymanish6.
- Tasks: keep retained lost tasks out of default status health counts, explain their cleanup window during maintenance, and prune lost task records after 24 hours instead of the general 7-day terminal retention.
- Memory-core: keep REM dreaming focused on live light-staged memories and mark staged entries as considered so old recall history no longer dominates fresh candidates. (#86302) Thanks @SebTardif.
- Memory: abort sync instead of downgrading an existing semantic vector index to FTS-only when the configured embedding provider is temporarily unavailable. (#85704) Thanks @yaaboo-gif.
- Telegram: propagate forum topic names through the account-scoped topic cache for native command context and topic create/edit actions. (#86299) Thanks @SebTardif.
- Slack: keep downloaded read-only files out of reply media so Slack file reads do not echo files back to the conversation. (#86318) Thanks @neeravmakwana.
- Cron: accept leading-plus relative durations such as `+5m` for one-shot `--at` schedules. (#86341) Thanks @mushuiyu886.
- Agents/media: preserve async-started media tool metadata so background generation starts no longer surface generic incomplete-turn warnings while replay stays unsafe. (#85933) Thanks @fuller-stack-dev.
- Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.
- QA/diagnostics: add a collector-backed OpenTelemetry smoke lane, make the OTLP payload leak check scenario-aware, and keep source QA builds from failing on optional dependency imports resolved through pnpm's temp module path.
- Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run `pnpm check:changed` from the intended diff.
- xAI/LM Studio: avoid buffering ordinary bracketed or `final` prose until stream completion while watching for plain-text tool-call fallbacks.
- Doctor: warn and continue when the cron job store exists but cannot be read so later health checks still run. Fixes #86102. (#86384) Thanks @1052326311.
- Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.
- Discord: restore bare numeric channel IDs for outbound message-tool sends while keeping explicit DM targets unambiguous. (#86571) Thanks @joshavant.
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
- Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`.
- Tests: give QA config mutation RPCs enough native Windows budget to finish gateway config writes and restart settle after hot scenario runs.
- Tests: keep the gateway restart-inflight QA scenario focused on restart recovery on native Windows by allowing expected embedded prompt handoff errors and using the Windows-safe timeout budget.
- QA-Lab: make the synthetic OpenAI provider honor generic `reply exactly:` directives after required kickoff reads so restart-recovery scenarios do not fall through to generic repo-summary prose.
- Gateway: abort active `agent` RPC runs during forced restart shutdown so stale in-process turns cannot keep writing a session after the Gateway lifecycle restarts.
- Crabbox: sync clean sparse worktrees through a temporary full checkout even when reusing an existing lease so tracked build-time files are not omitted.
- Build: route `scripts/ui.js` through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce `dist/control-ui`.
- Tests: give the memory fallback QA scenario enough turn budget to exercise native Windows gateway runs instead of failing on the client timeout while the mock agent is still dispatching.
- Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof.
- Install/update: bypass npm `min-release-age` policies with `--min-release-age=0` instead of `--before` so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.
- Diagnostics: reclaim wedged session lanes when stale active-run bookkeeping blocks queued work despite no forward progress. Fixes #85639. Thanks @openperf.
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.
- Gateway/perf: fail startup benchmark samples when the Gateway process exits before benchmark teardown, including signal deaths after readiness probes.
- Gateway/perf: fail restart benchmark samples when the Gateway exits before benchmark teardown, including clean exits and signal deaths after successful restart probes.
- Agents/tests: keep model catalog visibility on static selection helpers so catalog visibility checks avoid the broad model-selection barrel import.
- Agents/commitments: serialize commitment store load-modify-save writes so concurrent heartbeat and CLI updates no longer lose dismissal, sent, or attempt state. (#81153) Thanks @ai-hpc.
- xAI/LM Studio: promote plain-text tool-call fallbacks into structured tool calls and strip leaked internal tool syntax before user-facing delivery. (#86222) Thanks @fuller-stack-dev.
- CLI: suppress benign self-update version-skew warnings during package post-update finalization.
- Gateway/perf: tighten restart and startup benchmark failure handling so long profiling runs, failed probes, and fresh Linux runners no longer produce false passing or `n/a` results.
- Checks: keep intentional Knip unused-file findings optional so full CI and sparse proof workspaces stay aligned.
- Docker: restore writable `~/.config` in runtime images. Fixes #85968. Thanks @hkoessler and @Bartok9.
- Plugin SDK: keep legacy root diagnostic subscriptions connected when built plugin SDK aliases resolve diagnostic helpers through a separate module graph.
- Diagnostics: export alertable OTel and Prometheus signals for blocked tools, model failover, stale sessions, liveness warnings, oversized payloads, and webhook ingress while fixing shared OTLP endpoints with query strings.
- Tests: normalize macOS canonical temp paths in exec allowlists, fs-safe trash assertions, installed plugin matching, Telegram topic-name stores, and built ACPX MCP server expectations so native macOS proof runners cover the intended behavior.
- Codex/app-server: preserve message-tool-only source reply delivery mode on active runs so sub-agent completion wakeups can steer the active Codex turn instead of being rejected. (#86287) Thanks @ferminquant.
- Tests: sample the Windows kitchen-sink RPC gateway directly and serialize RSS probes so native runs keep the memory guard active.
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
- Agents/Claude CLI: route live native Bash permission requests through OpenClaw exec policy so Claude turns no longer stall on `control_request`, and document that OpenClaw exec policy is authoritative. Fixes #80819. (#86330, from #81971) Thanks @guthirry and @sallyom.
- Security audit: warn when YOLO OpenClaw exec policy overrides a restrictive raw Claude `--permission-mode` for managed live sessions. (#86557) Thanks @sallyom.
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
- Codex harness: make subscription usage-limit errors without reset times explain that OpenClaw cannot determine the reset and point users to wait until Codex is available, use another Codex account, or switch to another configured model/provider. Thanks @amknight.
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
- Telegram: route normal `[telegram][diag]` polling diagnostics through `runtime.log` while keeping non-diag warnings and persistence failures on `runtime.error`, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.
- Providers/Ollama: strip inline Kimi cloud reasoning prefixes from streamed and final visible replies while keeping ordinary Kimi answers append-only. (#86286) Thanks @jason-allen-oneal.
- Gateway: require Talk secret authority before setup-code handoff can include Talk secrets. (#85690) Thanks @ngutman.
- Agents: keep fallback error reporting scoped to the active model candidate so stale prior-provider quota/auth text is not reported for later fallback attempts. (#86134) thanks @zhangguiping-xydt.
- iMessage: dedupe watcher startup when `channels.imessage.accounts` lists both `default` and a named account that point at the same local Messages source, so the gateway no longer spawns two `imsg rpc` processes or doubles inbound replies; the dedupe is scoped to watcher startup, leaving duplicate accounts addressable for outbound sends, status, and capability listings, and `openclaw doctor` flags the redundant account with a rebinding hint. Fixes #65141. (#86705) Thanks @swang430.
## 2026.5.25
### Fixes
- Installer: let the local-prefix CLI installer use Alpine's `apk` Node.js, npm, and Git packages on musl Linux instead of downloading glibc Node tarballs that fail `node:sqlite`.
- Checks: prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs.
- Plugins: allow linked local plugin paths to probe TypeScript source entries without requiring compiled package output, restoring source-checkout plugin development on native Windows.
- CLI: route source-checkout build output to stderr before launching OpenClaw commands so stale local builds do not corrupt `--json` stdout.
- Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path.
- Agents/perf: cache manifest-backed CLI provider descriptors and fallback provider resolution so model fallback retries avoid repeated bundled provider runtime scans while still invalidating across plugin reloads.
- Installer: detect musl Linux shells such as Alpine as Linux instead of rejecting them before npm install.
- Windows: run direct Node package scripts with env overrides through a cross-platform launcher so gateway, TUI, Docker-all, generated-module formatting, and optional Discord native opus installer entrypoints work on native Windows.
- Tests: run Vitest import timing entrypoints through a Node wrapper so native Windows package scripts can collect import diagnostics.
- Control UI: split large build-time runtime dependencies into stable chunks so Linux/Docker install and package builds stay below the app chunk warning threshold.
- Tests: run `test:max` and `test:changed:max` through a Node wrapper so high-worker Vitest entrypoints work on native Windows.
- Tests: retry transient loopback HTTP resets in the kitchen-sink RPC walk so native Windows readiness probes do not fail after the gateway is already ready.
- Tests: run `test:serial` through a Node wrapper so targeted serial Vitest commands work on native Windows.
- Tests: normalize Vitest config path assertions so the infra config suite runs on native Windows paths.
- Installer: avoid the incompatible generated `--before` install filter when raw npm `min-release-age` config is present. (#85491) Thanks @TurboTheTurtle.
- Agents/MCP: bound bundled MCP `tools/list` catalog discovery so hung MCP servers do not block session tool materialization. (#85063) Thanks @nxmxbbd.
- Channels/iMessage: recover malformed anchorless group watch payloads by GUID before debounce/routing, and drop unrecoverable payloads instead of replying to the sender DM. Fixes #84470. Refs #84503. Thanks @zhangguiping-xydt and @zqchris.
- Channels/iMessage: advance the startup catchup cursor from live-handled rows after a completed catchup pass, including rows received while catchup is still running, so restarts do not replay them. (#85475) Thanks @TurboTheTurtle.
- Tests: mount the shared Windows command helper into bare Docker E2E harness containers so published upgrade-survivor config walks can start on Linux.
- Tests: keep the plugin binding command escape Docker smoke focused on its intended Vitest cases and skip source-only install lifecycle scripts.
- Tests: let the generic plugin install E2E assertions use a configurable temp root and Windows home-relative install paths.
- Tests: keep kitchen-sink plugin assertion fixtures on a configurable temp root so native Windows runs no longer skip full-surface diagnostic coverage.
- Tests: fail Gateway startup benchmarks when a child startup never produces ready probes or process metrics instead of reporting all `n/a` samples as passing.
- Config/secrets: allow exec SecretRef ids to include `#` selectors so AWS-style `secret#json_key` ids validate consistently. (#80731) Thanks @TurboTheTurtle.
- Tests: keep the Telegram user credential helper on platform temp and path APIs so native Windows credential export and restore commands do not write through POSIX-only paths.
- Installer: include the optional verify phase in the progress counter so `--verify` shows `[4/4] Verifying installation` instead of `[4/3]`.
- Crabbox: let the wrapper find a sibling Crabbox checkout from linked Git worktrees so Codex worktrees can run remote gates without a PATH shim.
- CI: tolerate the standard `--` option separator in shared helper flag parsing so perf and test commands accept package-manager argument forwarding.
- Tests: preserve `--` passthrough arguments in live-media, live-shard, and extension batch harnesses so Vitest filters are not misread or silently ignored.
- Crabbox: default AWS macOS runner requests to on-demand capacity so EC2 Mac proof commands do not fail on the unsupported Spot market default.
- Tests: run upgrade-survivor config recipe commands through the Windows npm shim so native Windows package walks keep baseline config coverage.
- Image tool: use bundled Anthropic media limits when resolving image compression policy without provider-runtime hooks.
- Tests: fail the kitchen-sink RPC Docker walk when gateway RSS sampling is unavailable instead of silently disabling the per-process memory guard.
- Tests: suppress the current Rolldown plugin timing warning format in the Vitest wrapper so tiny focused runs do not drown useful stderr in repeated build-timing noise.
- Models/OpenRouter: use endpoint-specific OpenRouter context limits from `top_provider` metadata so provider-routed models no longer overstate available context. (#85949) Thanks @TurboTheTurtle.
@@ -21,11 +192,15 @@ Docs: https://docs.openclaw.ai
- Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
- Auth/Codex: emit a one-shot actionable `log.warn` from the embedded legacy Codex OAuth sidecar loader when the only available seed lives in the macOS Keychain, naming `openclaw doctor --fix` and macOS Keychain instead of letting the credential silently fall through to a downstream `No API key found for provider "openai-codex"`. Thanks @romneyda.
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
- Control UI/agents: keep collapsed tool rows readable without early ellipses, preserve raw expanded tool details, and make post-compaction AGENTS.md reinjection opt-in to avoid duplicated project context. Fixes #45649 and #45488. Thanks @BunsDev.
## 2026.5.24
### Changes
- iMessage: support thumb-approval reactions — `👍` (Like tapback) resolves an approval as `allow-once` and `👎` resolves as `deny`, with the explicit-approver allowlist read from `channels.imessage.allowFrom`; `allow-always` stays on the manual `/approve <id> allow-always` text fallback. Mirrors the WhatsApp behavior from #85477.
- Gateway/perf: reuse process-stable channel catalog reads, avoid repeated bundled-channel boundary checks, and rotate gateway watch CPU profiles so benchmark runs do not accumulate unbounded artifacts.
- Gateway/perf: cache stable install-record, channel-catalog, bundled-channel, and Telegram session-store metadata during process-local hot paths to reduce repeated JSON and manifest reads.
- Gateway/perf: reuse immutable plugin metadata snapshots across startup, config, model, channel, setup, and secret metadata readers so hot paths avoid repeated plugin file stats and manifest registry reloads.
@@ -66,6 +241,7 @@ Docs: https://docs.openclaw.ai
- Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.
- Plugins/SDK: add a general `embeddingProviders` capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.
- Dependencies: refresh provider, plugin, UI, and tooling packages, update `protobufjs` to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to `@agentclientprotocol/claude-agent-acp` 0.36.1.
- ACPX: bump the bundled ACP backend to `acpx` 0.10.0 for session export/import support.
- Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.
- QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.
- QA-Lab: add a QA bus tool-trace visibility scenario for sanitized tool-call assertions.
@@ -85,7 +261,9 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/update: allow package-manager-managed hardlinked package roots during global update swaps while keeping generic plugin, hook, and dependency-free install moves fail-closed. (#85569) Thanks @ai-hpc.
- Gateway/update: avoid fetching unrelated tags during dev-channel git updates so moved release tags do not block branch-based updates. (#84737) Thanks @rubencu.
- CLI/update: suppress the expected future-config warning while an old update parent hands off to the freshly installed post-core process.
- MiniMax: store OAuth token expiry as an absolute millisecond timestamp so OAuth profiles no longer appear expired on every request. (#83480) Thanks @NianJiuZst.
- Agents/Anthropic: strip missing or blank thinking signatures for signed-thinking providers even when recovery supplies a narrow replay policy without signature preservation. Fixes #84430. (#84448) Thanks @NianJiuZst.
- Agents/channels: send a visible notice when an aborted main session cannot be resumed after restart, including Telegram group targets. (#85805) Thanks @pfrederiksen.
@@ -112,11 +290,13 @@ Docs: https://docs.openclaw.ai
- CLI/plugins: tighten timeout, numeric option, media payload, permission, profile/TLS, plugin metadata, JSON, and remote URL handling; prevent stuck progress/app-server/IRC/Synology/Twitch waits; and keep imported chat history ordering stable.
- Telegram/config: suppress the missing `accounts.default` warning when `channels.telegram.defaultAccount` names a configured account that also sorts first. Fixes #83948. Thanks @crypto86m.
- Telegram: serialize visible topic replies through core reply-lane admission so heartbeat and queued follow-up turns cannot continue ownerless or misroute responses. (#85709) Thanks @jalehman.
- CLI/node: print node status recovery hints on stdout consistently while keeping status errors on stderr. Fixes #83925. Thanks @davinci282828.
- WebChat: summarize internal message-tool source replies so tool cards no longer duplicate the visible reply body. (#84773) Thanks @jason-allen-oneal.
- Gateway/WebChat: hide duplicate `gateway-injected` assistant rows when Cursor ACP already persisted the same `acp-runtime` reply. Fixes #85741. Thanks @lxf-lxf.
- WebChat: scope the visible attachment button to its own composer file input so clicking Upload reliably opens the file picker. (#83952, fixes #47983) Thanks @jason-allen-oneal.
- Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong.
- Gateway/update: stop treating inherited macOS `XPC_SERVICE_NAME` values as launchd supervision during update respawn, so GUI-spawned gateways use detached respawn instead of exiting for a missing LaunchAgent. Fixes #85224. Thanks @richardmqq.
- Gateway: stop sending duplicate message-phase `sessions.changed` websocket events after displayable `session.message` transcript updates. (#84834)
- Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output.
- Telegram/ACP: preserve explicit `:topic:` conversation suffixes when inbound ACP targets do not carry a separate thread id.
- Browser/proxy: bypass the managed proxy for the exact local managed Chrome CDP readiness and DevTools WebSocket endpoints, so `openclaw browser start` works when the operator proxy blocks loopback egress. (#83255) Thanks @lightcap.
@@ -147,6 +327,7 @@ Docs: https://docs.openclaw.ai
- Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.
- Channels: honor `/verbose on` for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.
- Update: keep the detached gateway restart handoff best-effort when the restart script process cannot be spawned. (#83892) Thanks @davinci282828.
- Windows/config: skip POSIX login-shell env fallback on native Windows so startup no longer warns about missing `/bin/sh`. Fixes #84795. Thanks @JIRBOY.
- Telegram: persist the prompt-context message cache through plugin state and record bot-authored replies after sends and draft streaming so later turns can include prior assistant replies without relying on the JSON sidecar. (#85231) Thanks @keshavbotagent.
- Agents/subagents: keep Codex persona and user workspace files turn-scoped so native Codex subagents inherit only shared tool guidance by default. (#85811) Thanks @lastguru-net.
- CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.
@@ -188,6 +369,7 @@ Docs: https://docs.openclaw.ai
- TUI: keep quiet active runs busy after the response watchdog notice instead of reopening the prompt and encouraging duplicate submissions while the backend turn is still running. Thanks @shakkernerd.
- Agents: preserve the latest assistant thinking blocks while stripping invalid replay signatures from older turns, and retry Anthropic thinking failures without thinking replay. Fixes #85557. Thanks @bryanbaer.
- Agents: keep parallel OpenAI-compatible tool-call deltas in separate argument buffers so interleaved tool calls no longer corrupt streamed arguments. (#82263) Thanks @luna-system.
- Telegram: avoid false pairing prompts after transient pairing-store read failures while preserving configured `allowFrom` and per-DM pairing authorization. (#85555)
- Memory/doctor: report missing or unusable QMD workspace directories as workspace failures instead of generic binary failures. (#63167) Thanks @sercada.
- Debug proxy: record CONNECT client-socket errors and destroy the paired upstream socket so abrupt client disconnects no longer leak tunnel resources. (#82444) Thanks @SebTardif.
- Diffs: continue hydrating later diff cards when one card fails so a single broken card no longer blanks the whole diff viewer. (#84775) Thanks @cosmopolitan033.
@@ -197,7 +379,7 @@ Docs: https://docs.openclaw.ai
- Gateway/agents: preserve fresh session overrides and metadata when stale cached agent-session entries race with store updates, so subagent model/provider overrides and routing policy survive concurrent writes. (#19328) Thanks @CodeReclaimers.
- Control UI/chat: keep chat session search inline with the session selector so the header no longer shows a duplicate standalone search row.
- Control UI/chat: collapse focused-mode header chrome and suppress hidden-header scroll updates so focus mode no longer jumps while scrolling. Thanks @amknight.
- Codex app-server: restart the native app-server and retry once when server-side compaction times out, so preflight compaction stalls recover instead of failing every dispatch. (#85500)
- Codex app-server: leave automatic compaction to native Codex, drop OpenClaw preflight/CLI/context-engine forced compaction for Codex runtime sessions, and still forward explicit `/compact` or plugin compaction requests into Codex while failing native compaction honestly. (#85500)
- Restore Control UI gateway token pairing [AI]. (#85459) Thanks @pgondhi987.
- OpenAI video: honor configured provider request private-network opt-in for local/custom video endpoints so explicitly trusted mock and self-hosted providers are not blocked. Thanks @shakkernerd.
- OpenAI video: send uploaded video edit requests to the documented `/videos/edits` endpoint with a `video` file instead of posting MP4 references to `/videos`. Thanks @shakkernerd.
@@ -282,7 +464,7 @@ Docs: https://docs.openclaw.ai
- fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.
- Control UI: keep the chat session picker from hiding older or cross-agent configured conversations while preserving the bounded configured-agent refresh. (#85211) Thanks @amknight.
- Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.
- Agents/Codex: estimate tool-heavy prompt pressure at the LLM boundary before provider submission, so persistent sessions compact before overflowing context windows. (#85541) Thanks @fuller-stack-dev and @joshavant.
- Agents: estimate tool-heavy prompt pressure at the LLM boundary before provider submission for non-Codex embedded runtimes, so persistent PI-style sessions compact before overflowing context windows. (#85541) Thanks @fuller-stack-dev and @joshavant.
- Agents/hooks: wait for local one-shot CLI and Codex `agent_end` plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)
- Providers/Google: preserve Gemini 3 cron `thinkingDefault: "low"` when stale catalog metadata says `reasoning:false`, so scheduled runs keep provider-supported thinking instead of downgrading to off. (#85185) Thanks @neeravmakwana.
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.
@@ -382,6 +564,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Control UI/WebChat: keep selected external-channel sessions live by mirroring Codex prompts at turn start, streaming hidden runs only to exact selected-session subscribers, and deduplicating accumulated stream snapshots around tool cards. Fixes #83528, #82611, refs #83949. Thanks @BunsDev.
- CLI/tasks: include stale-running task maintenance decisions in `openclaw tasks maintenance --json` so retained and reconcile candidates explain backing-session, cron, CLI, and wedged-subagent state. (#84691) Thanks @efpiva.
- Codex app-server: keep system-prompt reports working when bootstrap hooks provide workspace files with only a path and content, so hook-supplied SOUL/IDENTITY/TOOLS/USER context still reports injected characters correctly. (#84736) Thanks @JARVIS-Glasses.
- Providers/MiniMax music: stop advertising `durationSeconds` control and remove prompt-injected duration hints, so `music_generate` reports MiniMax duration as an unsupported override instead of suggesting MiniMax can enforce track length. Fixes #84508. Thanks @neeravmakwana.
@@ -518,6 +701,7 @@ Docs: https://docs.openclaw.ai
- CLI: reject explicit port numbers above 65535 before they reach Gateway or Node bind paths. Fixes #83900. (#84008) Thanks @hclsys.
- Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu.
- Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss.
- Telegram: preserve inbound bold, italic, code, preformatted, strikethrough, underline, spoiler, and text-link entities as markdown in the agent-facing prompt body. Fixes #52859.
- Backup: dereference hardlinks during archive creation and reject unsafe hardlink targets during verification so archives that pass `backup verify` do not fail broad extraction on macOS tar. Fixes #54242. Thanks @jason-allen-oneal.
- Memory Wiki: preserve fs-safe diagnostics when bridge source page writes fail for non-symlink filesystem safety reasons, so directory collisions are reported with the underlying error code. (#83776) Thanks @TurboTheTurtle.
- Telegram: keep forum topics from blocking sibling topic traffic by routing inbound serialization, media/text buffers, and account API queues on topic-aware lanes. (#83829)
@@ -1989,6 +2173,7 @@ Docs: https://docs.openclaw.ai
- Telegram/groups: include the recent local chat window and nearby reply-target window as generic inbound context so stale reply ancestry does not overshadow the live group conversation.
- Plugins/Nix: allow externally configured plugin roots under `/nix/store` to load in `OPENCLAW_NIX_MODE=1` while keeping normal external plugin hardlink rejection unchanged. Thanks @joshp123.
- Nextcloud Talk: include the required bot `response` feature in setup, explain missing `--feature response` on rejected sends, and surface missing response capability in doctor/status checks. Fixes #78935. (#79657) Thanks @joshavant.
- Cron/diagnostics: emit the existing `message.queued`, `session.state` (processing/idle), and `message.processed` lifecycle events for isolated-cron agent turns in `runCronIsolatedAgentTurn`, matching the dispatch and embedded-runner paths so subscribers (diagnostics OTLP, OTel exporters, custom observability plugins) get per-run session attribution instead of bucketing isolated cron LLM calls under static fallback ids. Events are gated on `isDiagnosticsEnabled(cfg)` so the documented `diagnostics.enabled: false` master toggle continues to silence the recorder. (#79214) Thanks @arniesaha.
- fix(discord): gate user allowlist name resolution [AI]. (#79002) Thanks @pgondhi987.
- fix(msteams): gate startup user allowlist resolution [AI]. (#79003) Thanks @pgondhi987.
- Infra/fetch-timeout: pass `operation` and `url` context to `buildTimeoutAbortSignal` from the music-generate reference fetch and the Matrix guarded redirect transport, so the `fetch timeout reached; aborting operation` warning carries actionable structured fields instead of a bare line. Fixes #79195. Thanks @pandadev66.

View File

@@ -107,6 +107,7 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
- Test locally with your OpenClaw instance
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
- Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR.
- Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes.
- Run tests: `pnpm build && pnpm check && pnpm test`
- For iterative local commits, `scripts/committer --fast "message" <files...>` passes `FAST_COMMIT=1` through to the pre-commit hook so it skips the repo-wide `pnpm check`. Only use it when you've already run equivalent targeted validation for the touched surface.

View File

@@ -287,12 +287,17 @@ RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
# Pre-create default named-volume mount points so first-run Docker volumes copy
# node ownership from the image instead of starting as root-owned directories.
RUN install -d -m 0700 -o node -g node \
# NOTE: /home/node/.config must be created with node ownership first so that
# the leaf /home/node/.config/openclaw inherits the correct parent permissions.
# Without this, install -d leaves /home/node/.config as root:root (issue #85968).
RUN install -d -m 0755 -o node -g node /home/node/.config && \
install -d -m 0700 -o node -g node \
/home/node/.openclaw \
/home/node/.openclaw/workspace \
/home/node/.config/openclaw && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.openclaw/workspace | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.config | grep -qx 'node:node 755' && \
stat -c '%U:%G %a' /home/node/.config/openclaw | grep -qx 'node:node 700'
ENV NODE_ENV=production

View File

@@ -98,7 +98,7 @@ These are frequently reported but are typically closed with no code change:
- Reports that treat `POST /tools/invoke` under shared-secret bearer auth (`gateway.auth.mode="token"` or `"password"`) as a narrower per-request/per-scope authorization surface. That endpoint is designed as the same trusted-operator HTTP boundary: shared-secret bearer auth is full operator access there, narrower `x-openclaw-scopes` values do not reduce that path, and owner-only tool policy follows the shared-secret operator contract.
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- Reports that only show an ACP tool can indirectly execute, mutate, orchestrate sessions, or reach another tool/runtime without demonstrating bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. ACP silent approval is intentionally limited to narrow readonly classes; parity-only indirect-command findings are hardening, not vulnerabilities.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example Sharp/libvips/libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example image codec libraries such as libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports whose only impact is transient extra memory, CPU, or allocation work from decoding, base64 expansion, media transcoding, serialization, or other format conversion after the input was already accepted under OpenClaw's configured size/trust limits, including base64 decode-before-size-estimate findings. These are performance issues, not vulnerabilities, unless the report demonstrates unauthenticated amplification, bypass of configured limits, crash/process termination, persistent resource exhaustion, data exposure, or another documented boundary bypass.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.

View File

@@ -38,6 +38,15 @@ Contribution rules:
- Do not open large batches of tiny PRs at once; each PR has review cost.
- For very small related fixes, grouping into one focused PR is encouraged.
Configuration compatibility:
OpenClaw runtime code reads the current configuration schema only.
We do not keep long-lived aliases or compatibility branches that silently accept old, renamed, or malformed config keys.
When a config change makes existing user config invalid, the same change needs a doctor migration.
`openclaw doctor --fix` should detect the old shape, explain it, back it up when needed, and rewrite it to the canonical format.
Core-owned config and auth state are repaired in core doctor code; plugin-owned config is repaired by that plugin's doctor contract.
## Security
Security in OpenClaw is a deliberate tradeoff: strong defaults without killing capability.

View File

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

View File

@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<receiver
android:name=".VoiceE2eReceiver"
android:exported="true">
<intent-filter>
<action android:name="ai.openclaw.app.debug.RUN_VOICE_E2E" />
</intent-filter>
</receiver>
<service
android:name=".VoiceE2eService"
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,195 @@
package ai.openclaw.app
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import java.io.File
private const val tag = "VoiceE2E"
private const val resultFileName = "voice_e2e_result.json"
class VoiceE2eReceiver : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent,
) {
context.startService(
Intent(context, VoiceE2eService::class.java)
.putExtras(intent),
)
}
}
class VoiceE2eService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
val command = intent ?: return START_NOT_STICKY
serviceScope.launch {
try {
runCommand(command)
} finally {
stopSelf(startId)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
serviceScope.cancel()
super.onDestroy()
}
private suspend fun runCommand(intent: Intent) {
try {
val app = applicationContext as NodeApp
val runtime = app.ensureRuntime()
val mode =
intent
.getDecodedStringExtra("mode")
?.trim()
.orEmpty()
.ifEmpty { "both" }
if (mode == "stop") {
runtime.cancelMicCapture()
runtime.setTalkModeEnabled(false)
writeResult("""{"ok":true,"mode":"stop"}""")
return
}
val connect = !intent.getBooleanExtra("noConnect", false)
val connectTimeoutMs = intent.getLongExtra("connectTimeoutMs", 20_000L)
if (connect) {
configureGateway(runtime = runtime, intent = intent)
}
if (connect || !runtime.isConnected.value) {
awaitGateway(runtime = runtime, timeoutMs = connectTimeoutMs)
}
startActivity(
Intent(actionOpenVoiceE2e)
.setClass(this, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
)
if (mode == "connect") {
val resultJson = """{"ok":true,"mode":"connect","connected":true}"""
writeResult(resultJson)
Log.i(tag, "PASS $resultJson")
return
}
val transcript =
intent
.getDecodedStringExtra("transcript")
?.trim()
.orEmpty()
.ifEmpty { "Reply exactly: Android voice e2e normal path ok." }
val realtimeReply =
intent
.getDecodedStringExtra("realtimeAssistant")
?.trim()
.orEmpty()
.ifEmpty { "Android realtime voice e2e relay path ok." }
val timeoutMs = intent.getLongExtra("timeoutMs", 60_000L)
val result =
runtime.runVoiceE2e(
mode = mode,
transcript = transcript,
realtimeAssistantText = realtimeReply,
timeoutMs = timeoutMs,
)
val resultJson = encodeResult(result)
writeResult(resultJson)
Log.i(tag, "PASS $resultJson")
} catch (err: Throwable) {
val resultJson =
buildJsonObject {
put("ok", JsonPrimitive(false))
put("error", JsonPrimitive(err.message ?: err::class.java.simpleName))
}.toString()
writeResult(resultJson)
Log.e(tag, "FAIL $resultJson", err)
}
}
private fun configureGateway(
runtime: NodeRuntime,
intent: Intent,
) {
val host =
intent
.getDecodedStringExtra("host")
?.trim()
.orEmpty()
.ifEmpty { "127.0.0.1" }
val port = intent.getIntExtra("port", 18789)
runtime.setManualEnabled(true)
runtime.setManualHost(host)
runtime.setManualPort(port)
runtime.setManualTls(intent.getBooleanExtra("tls", false))
runtime.setGatewayToken(intent.getDecodedStringExtra("token").orEmpty())
runtime.setGatewayBootstrapToken(intent.getDecodedStringExtra("bootstrapToken").orEmpty())
runtime.setGatewayPassword(intent.getDecodedStringExtra("password").orEmpty())
runtime.setOnboardingCompleted(true)
runtime.connectManual()
}
private suspend fun awaitGateway(
runtime: NodeRuntime,
timeoutMs: Long,
) {
withTimeout(timeoutMs) {
while (!runtime.isConnected.value) {
delay(100L)
}
}
}
private fun encodeResult(result: NodeRuntime.VoiceE2eResult): String =
buildJsonObject {
put("ok", JsonPrimitive(true))
put("normal", result.normal?.let(::encodeSlice) ?: JsonNull)
put("realtime", result.realtime?.let(::encodeSlice) ?: JsonNull)
}.toString()
private fun encodeSlice(slice: NodeRuntime.VoiceE2eSliceResult) =
buildJsonObject {
put("mode", JsonPrimitive(slice.mode))
put("status", JsonPrimitive(slice.status))
put("userText", slice.userText?.let(::JsonPrimitive) ?: JsonNull)
put("assistantText", slice.assistantText?.let(::JsonPrimitive) ?: JsonNull)
}
private fun writeResult(json: String) {
File(cacheDir, resultFileName).writeText(json)
}
}
private fun Intent.getDecodedStringExtra(name: String): String? {
val encoded = getStringExtra("${name}Base64")
if (!encoded.isNullOrBlank()) {
return String(Base64.decode(encoded, Base64.NO_WRAP), Charsets.UTF_8)
}
return getStringExtra(name)
}

View File

@@ -3,6 +3,7 @@ package ai.openclaw.app
import android.content.Intent
const val actionAskOpenClaw = "ai.openclaw.app.action.ASK_OPENCLAW"
const val actionOpenVoiceE2e = "ai.openclaw.app.debug.OPEN_VOICE_E2E"
const val extraAssistantPrompt = "prompt"
enum class HomeDestination {
@@ -19,6 +20,14 @@ data class AssistantLaunchRequest(
val autoSend: Boolean,
)
fun parseHomeDestinationIntent(intent: Intent?): HomeDestination? {
val action = intent?.action ?: return null
return when {
BuildConfig.DEBUG && action == actionOpenVoiceE2e -> HomeDestination.Voice
else -> null
}
}
fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
val action = intent?.action ?: return null
return when (action) {

View File

@@ -79,6 +79,10 @@ class MainActivity : ComponentActivity() {
}
private fun handleAssistantIntent(intent: android.content.Intent?) {
parseHomeDestinationIntent(intent)?.let { destination ->
viewModel.requestHomeDestination(destination)
return
}
val request = parseAssistantLaunchIntent(intent) ?: return
viewModel.handleAssistantLaunch(request)
}

View File

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

View File

@@ -47,6 +47,7 @@ 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 ai.openclaw.app.voice.VoiceConversationRole
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
@@ -64,6 +65,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
@@ -256,6 +258,18 @@ class NodeRuntime(
val previousFingerprintSha256: String? = null,
)
data class VoiceE2eSliceResult(
val mode: String,
val status: String,
val userText: String?,
val assistantText: String?,
)
data class VoiceE2eResult(
val normal: VoiceE2eSliceResult?,
val realtime: VoiceE2eSliceResult?,
)
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
private val _nodeConnected = MutableStateFlow(false)
@@ -501,7 +515,7 @@ class NodeRuntime(
context = appContext,
scope = scope,
session = operatorSession,
isConnected = { operatorConnected },
isConnected = { _isConnected.value },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
).also { speaker ->
@@ -608,7 +622,7 @@ class NodeRuntime(
context = appContext,
scope = scope,
session = operatorSession,
isConnected = { operatorConnected },
isConnected = { _isConnected.value },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
onStoppedByRelay = { finishTalkModeAfterRelayClose() },
@@ -855,6 +869,7 @@ class NodeRuntime(
val chatSessionKey: StateFlow<String> = chat.sessionKey
val chatSessionId: StateFlow<String?> = chat.sessionId
val chatMessages: StateFlow<List<ChatMessage>> = chat.messages
val chatHistoryLoading: StateFlow<Boolean> = chat.historyLoading
val chatError: StateFlow<String?> = chat.errorText
val chatHealthOk: StateFlow<Boolean> = chat.healthOk
val chatThinkingLevel: StateFlow<String> = chat.thinkingLevel
@@ -1186,6 +1201,115 @@ class NodeRuntime(
talkMode.setPlaybackEnabled(value)
}
suspend fun runVoiceE2e(
mode: String,
transcript: String,
realtimeAssistantText: String,
timeoutMs: Long,
): VoiceE2eResult {
if (!BuildConfig.DEBUG) {
throw IllegalStateException("voice e2e is debug-only")
}
if (!_isConnected.value) {
throw IllegalStateException("gateway not connected")
}
if (!hasRecordAudioPermission()) {
throw IllegalStateException("microphone permission missing")
}
val normalizedMode = mode.trim().lowercase().ifEmpty { "both" }
val runNormal = normalizedMode == "both" || normalizedMode == "normal" || normalizedMode == "dictation"
val runRealtime = normalizedMode == "both" || normalizedMode == "realtime" || normalizedMode == "talk"
if (!runNormal && !runRealtime) {
throw IllegalArgumentException("unknown voice e2e mode: $mode")
}
val previousSpeakerEnabled = speakerEnabled.value
setSpeakerEnabled(false)
var completed = false
return try {
VoiceE2eResult(
normal =
if (runNormal) {
runNormalVoiceE2e(transcript = transcript, timeoutMs = timeoutMs)
} else {
null
},
realtime =
if (runRealtime) {
runRealtimeVoiceE2e(
transcript = transcript,
assistantText = realtimeAssistantText,
timeoutMs = timeoutMs,
)
} else {
null
},
).also { completed = true }
} finally {
if (!completed) {
stopActiveVoiceSession()
}
setSpeakerEnabled(previousSpeakerEnabled)
}
}
private suspend fun runNormalVoiceE2e(
transcript: String,
timeoutMs: Long,
): VoiceE2eSliceResult {
stopActiveVoiceSession()
setVoiceCaptureMode(VoiceCaptureMode.ManualMic)
micCapture.submitTranscribedMessage(transcript)
awaitVoiceConversation(timeoutMs = timeoutMs) {
micCapture.conversation.value.any { it.role == VoiceConversationRole.Assistant && !it.isStreaming }
}
val entries = micCapture.conversation.value
return VoiceE2eSliceResult(
mode = "normal",
status = micCapture.statusText.value,
userText = entries.lastOrNull { it.role == VoiceConversationRole.User }?.text,
assistantText = entries.lastOrNull { it.role == VoiceConversationRole.Assistant }?.text,
)
}
private suspend fun runRealtimeVoiceE2e(
transcript: String,
assistantText: String,
timeoutMs: Long,
): VoiceE2eSliceResult {
stopActiveVoiceSession()
setVoiceCaptureMode(VoiceCaptureMode.TalkMode)
talkMode.runE2eRealtimeTurn(
userText = transcript,
assistantText = assistantText,
timeoutMs = timeoutMs,
)
awaitVoiceConversation(timeoutMs = timeoutMs) {
val entries = talkMode.conversation.value
entries.any { it.role == VoiceConversationRole.User && !it.isStreaming } &&
entries.any { it.role == VoiceConversationRole.Assistant && !it.isStreaming }
}
val entries = talkMode.conversation.value
return VoiceE2eSliceResult(
mode = "realtime",
status = talkMode.statusText.value,
userText = entries.lastOrNull { it.role == VoiceConversationRole.User }?.text,
assistantText = entries.lastOrNull { it.role == VoiceConversationRole.Assistant }?.text,
)
}
private suspend fun awaitVoiceConversation(
timeoutMs: Long,
ready: () -> Boolean,
) {
withTimeout(timeoutMs) {
while (!ready()) {
delay(100L)
}
}
}
private fun setVoiceCaptureMode(
mode: VoiceCaptureMode,
persistManualMic: Boolean = true,
@@ -1443,7 +1567,7 @@ class NodeRuntime(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
if (operatorConnected || operatorStatusText == "Connecting…") {
if (operatorConnected) {
return
}
val operatorAuth =

View File

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

View File

@@ -90,6 +90,7 @@ private data class SelectedConnectAuth(
val authDeviceToken: String?,
val authPassword: String?,
val signatureToken: String?,
val storedScopes: List<String>,
val authSource: GatewayConnectAuthSource,
val attemptedDeviceTokenRetry: Boolean,
)
@@ -580,7 +581,8 @@ class GatewaySession(
private suspend fun sendConnect(connectNonce: String) {
val identity = identityStore.loadOrCreate()
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)?.trim()
val storedEntry = deviceAuthStore.loadEntry(identity.deviceId, options.role)
val storedToken = storedEntry?.token?.trim()
val selectedAuth =
selectConnectAuth(
endpoint = endpoint,
@@ -590,6 +592,7 @@ class GatewaySession(
explicitBootstrapToken = bootstrapToken?.trim()?.takeIf { it.isNotEmpty() },
explicitPassword = password?.trim()?.takeIf { it.isNotEmpty() },
storedToken = storedToken?.takeIf { it.isNotEmpty() },
storedScopes = storedEntry?.scopes.orEmpty(),
)
if (selectedAuth.attemptedDeviceTokenRetry) {
pendingDeviceTokenRetry = false
@@ -643,7 +646,6 @@ class GatewaySession(
val allowedOperatorScopes =
setOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
)
@@ -793,6 +795,7 @@ class GatewaySession(
else -> null
}
val connectScopes = resolveConnectScopes(selectedAuth)
val signedAtMs = System.currentTimeMillis()
val payload =
DeviceAuthPayload.buildV3(
@@ -800,7 +803,7 @@ class GatewaySession(
clientId = client.id,
clientMode = client.mode,
role = options.role,
scopes = options.scopes,
scopes = connectScopes,
signedAtMs = signedAtMs,
token = selectedAuth.signatureToken,
nonce = connectNonce,
@@ -839,7 +842,7 @@ class GatewaySession(
)
}
put("role", JsonPrimitive(options.role))
if (options.scopes.isNotEmpty()) put("scopes", JsonArray(options.scopes.map(::JsonPrimitive)))
if (connectScopes.isNotEmpty()) put("scopes", JsonArray(connectScopes.map(::JsonPrimitive)))
authJson?.let { put("auth", it) }
deviceJson?.let { put("device", it) }
put("locale", JsonPrimitive(locale))
@@ -849,6 +852,16 @@ class GatewaySession(
}
}
private fun resolveConnectScopes(selectedAuth: SelectedConnectAuth): List<String> {
if (selectedAuth.authSource == GatewayConnectAuthSource.BOOTSTRAP_TOKEN) {
return filteredBootstrapHandoffScopes(options.role, options.scopes).orEmpty()
}
if (selectedAuth.authSource == GatewayConnectAuthSource.DEVICE_TOKEN && selectedAuth.storedScopes.isNotEmpty()) {
return selectedAuth.storedScopes
}
return options.scopes
}
private suspend fun handleMessage(text: String) {
val frame = json.parseToJsonElement(text).asObjectOrNull() ?: return
when (frame["type"].asStringOrNull()) {
@@ -1122,6 +1135,7 @@ class GatewaySession(
explicitBootstrapToken: String?,
explicitPassword: String?,
storedToken: String?,
storedScopes: List<String>,
): SelectedConnectAuth {
val shouldUseDeviceRetryToken =
pendingDeviceTokenRetry &&
@@ -1155,6 +1169,7 @@ class GatewaySession(
authDeviceToken = authDeviceToken,
authPassword = explicitPassword,
signatureToken = authToken ?: authBootstrapToken,
storedScopes = storedScopes,
authSource = authSource,
attemptedDeviceTokenRetry = shouldUseDeviceRetryToken,
)

View File

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

View File

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

View File

@@ -128,6 +128,7 @@ fun OnboardingFlow(
val gateways by viewModel.gateways.collectAsState()
val savedToken by viewModel.gatewayToken.collectAsState()
val pendingTrust by viewModel.pendingGatewayTrust.collectAsState()
val startAtGatewaySetup by viewModel.startOnboardingAtGatewaySetup.collectAsState()
val ready = canFinishOnboarding(isConnected = isConnected, isNodeConnected = isNodeConnected)
var step by rememberSaveable { mutableStateOf(OnboardingStep.Welcome) }
@@ -153,6 +154,13 @@ fun OnboardingFlow(
val permissionState = rememberPermissionState(context = context, viewModel = viewModel)
LaunchedEffect(startAtGatewaySetup) {
if (startAtGatewaySetup) {
step = OnboardingStep.Gateway
viewModel.clearGatewaySetupStartRequest()
}
}
LaunchedEffect(ready, attemptedConnect) {
if (attemptedConnect && ready) {
step = OnboardingStep.Permissions
@@ -604,7 +612,11 @@ private fun PermissionSetupScreen(
) {
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween) {
LazyColumn(contentPadding = PaddingValues(bottom = 14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(bottom = 14.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
item {
PermissionTopBar(onBack = onBack)
}

View File

@@ -70,6 +70,7 @@ import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.QrCode2
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -90,6 +91,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
internal enum class SettingsRoute {
@@ -683,6 +685,13 @@ private fun GatewaySettingsScreen(
ClawPrimaryButton(text = "Reconnect", onClick = viewModel::refreshGatewayConnection, modifier = Modifier.weight(1f))
ClawSecondaryButton(text = "Disconnect", onClick = viewModel::disconnect, modifier = Modifier.weight(1f))
}
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Pair New Gateway", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Clear this phone's saved gateway access and scan a fresh setup code.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
ClawSecondaryButton(text = "Pair New Gateway", onClick = viewModel::pairNewGateway, modifier = Modifier.fillMaxWidth(), icon = Icons.Default.QrCode2)
}
}
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Connection Setup", style = ClawTheme.type.section, color = ClawTheme.colors.text)
@@ -1116,11 +1125,11 @@ private fun SettingsToggleListRow(row: SettingsToggleRow) {
@Composable
internal fun SettingsMetricPanel(rows: List<SettingsMetric>) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
ClawSeparatedColumn(items = rows) { row ->
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 50.dp).padding(horizontal = 0.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
Text(text = row.value, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = row.value, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}

View File

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

View File

@@ -161,7 +161,7 @@ fun VoiceScreen(
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
VoiceHeader(
statusText = if (voiceActive || !gatewayReady) activeStatus else "Your voice command center.",
@@ -371,12 +371,12 @@ private fun TalkSessionScreen(
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(11.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onEndTalk)
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 14.sp, lineHeight = 17.sp), color = ClawTheme.colors.text)
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (speaking || listening) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
Text(
@@ -397,8 +397,8 @@ private fun TalkSessionScreen(
}
Surface(
modifier = Modifier.fillMaxWidth().height(58.dp),
shape = RoundedCornerShape(ClawTheme.radii.pill),
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
@@ -429,7 +429,7 @@ private fun TalkTranscript(
entries: List<VoiceConversationEntry>,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(7.dp)) {
LazyColumn(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (entries.isEmpty()) {
item {
TalkTranscriptCard(label = "OpenClaw", text = "Listening for your next turn.", muted = true)
@@ -458,7 +458,7 @@ private fun TalkTranscriptCard(
color = ClawTheme.colors.surface,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(text = label, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = text, style = ClawTheme.type.body, color = if (muted) ClawTheme.colors.textMuted else ClawTheme.colors.text)
}
@@ -476,7 +476,7 @@ private fun TalkControl(
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
shape = RoundedCornerShape(ClawTheme.radii.button),
color = if (primary) ClawTheme.colors.primary else ClawTheme.colors.canvas,
contentColor = if (primary) ClawTheme.colors.primaryText else ClawTheme.colors.text,
border = BorderStroke(1.dp, if (primary) ClawTheme.colors.primary else ClawTheme.colors.border),
@@ -592,7 +592,7 @@ private fun VoiceHero(
onStartDictation: () -> Unit,
onConnectGateway: () -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(9.dp)) {
VoiceOrb(
active = micEnabled || talkModeEnabled,
listening = talkModeListening || voiceCaptureMode == VoiceCaptureMode.ManualMic,
@@ -639,7 +639,7 @@ private fun VoiceHero(
}
}
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
VoiceModeRow(
title = if (talkModeEnabled) "End Talk" else "Realtime Talk",
subtitle =
@@ -696,19 +696,19 @@ private fun VoiceModeRow(
) {
Surface(onClick = onClick, enabled = enabled, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 60.dp).padding(horizontal = 10.dp, vertical = 6.dp),
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp).padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
modifier = Modifier.size(30.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = if (enabled) ClawTheme.colors.surface else ClawTheme.colors.canvas,
contentColor = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(15.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -719,7 +719,7 @@ private fun VoiceModeRow(
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier.size(21.dp),
modifier = Modifier.size(18.dp),
tint = ClawTheme.colors.textMuted,
)
}
@@ -738,19 +738,19 @@ private fun VoiceProviderCard(gatewayStatus: String) {
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 9.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
modifier = Modifier.size(30.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(17.dp))
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(15.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -780,7 +780,7 @@ private fun VoicePrimaryAction(
Surface(
onClick = onClick,
modifier = Modifier.fillMaxWidth().height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.button),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
@@ -802,7 +802,7 @@ private fun VoiceOrb(
speaking: Boolean,
) {
Surface(
modifier = Modifier.size(132.dp),
modifier = Modifier.size(112.dp),
shape = CircleShape,
color = if (active) ClawTheme.colors.surfacePressed else ClawTheme.colors.surface,
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
@@ -817,7 +817,7 @@ private fun VoiceOrb(
else -> Icons.Default.Mic
},
contentDescription = null,
modifier = Modifier.size(38.dp),
modifier = Modifier.size(32.dp),
tint = ClawTheme.colors.text,
)
Waveform(active = active)
@@ -875,7 +875,7 @@ private fun VoiceTranscript(
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Live transcript", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 9.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 9.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "No transcript yet", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
@@ -902,7 +902,7 @@ private fun VoiceTurnCard(entry: VoiceConversationEntry) {
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, if (entry.isStreaming) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(
text = if (isUser) "You" else "OpenClaw",
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
internal enum class ClawStatus {
Neutral,
@@ -195,7 +196,7 @@ internal fun ClawStatusPill(
Surface(
modifier = modifier,
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = backgroundColor,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
@@ -207,11 +208,11 @@ internal fun ClawStatusPill(
Box(
modifier =
Modifier
.size(6.dp)
.size(5.dp)
.clip(CircleShape)
.background(dotColor),
)
Text(text = text, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
Text(text = text, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@@ -253,7 +254,7 @@ internal fun <T> ClawListPanel(
modifier: Modifier = Modifier,
row: @Composable (T) -> Unit,
) {
ClawPanel(modifier = modifier, contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(modifier = modifier, contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
ClawSeparatedColumn(items = items, row = row)
}
}
@@ -268,7 +269,7 @@ internal fun <T> ClawSeparatedColumn(
items.forEachIndexed { index, item ->
row(item)
if (index != items.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HorizontalDivider(color = ClawTheme.colors.border.copy(alpha = 0.82f), thickness = 1.dp)
}
}
}
@@ -286,8 +287,8 @@ internal fun ClawDetailRow(
modifier =
modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.padding(horizontal = 12.dp, vertical = 5.dp),
.heightIn(min = 54.dp)
.padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {

View File

@@ -262,6 +262,11 @@ class MicCaptureManager(
}
}
internal fun submitTranscribedMessage(text: String) {
queueRecognizedMessage(text)
sendQueuedIfIdle()
}
fun handleGatewayEvent(
event: String,
payloadJson: String?,
@@ -701,8 +706,7 @@ class MicCaptureManager(
val text = obj["text"].asStringOrNull()?.trim().orEmpty()
if (text.isNotEmpty()) {
if (text != flushedPartialTranscript) {
queueRecognizedMessage(text)
sendQueuedIfIdle()
submitTranscribedMessage(text)
} else {
flushedPartialTranscript = null
_liveTranscript.value = null

View File

@@ -11,14 +11,8 @@ internal data class TalkModeGatewayConfigState(
val mainSessionKey: String,
val interruptOnSpeech: Boolean?,
val silenceTimeoutMs: Long,
val executionMode: TalkModeExecutionMode,
)
internal enum class TalkModeExecutionMode {
Native,
RealtimeRelay,
}
internal object TalkModeGatewayConfigParser {
fun parse(config: JsonObject?): TalkModeGatewayConfigState {
val talk = config?.get("talk").asObjectOrNull()
@@ -27,22 +21,9 @@ internal object TalkModeGatewayConfigParser {
mainSessionKey = normalizeMainKey(sessionCfg?.get("mainKey").asStringOrNull()),
interruptOnSpeech = talk?.get("interruptOnSpeech").asBooleanOrNull(),
silenceTimeoutMs = resolvedSilenceTimeoutMs(talk),
executionMode = resolvedExecutionMode(talk),
)
}
fun resolvedExecutionMode(talk: JsonObject?): TalkModeExecutionMode {
val realtime = talk?.get("realtime").asObjectOrNull() ?: return TalkModeExecutionMode.Native
val mode = realtime["mode"].asStringOrNull()
val transport = realtime["transport"].asStringOrNull()
val brain = realtime["brain"].asStringOrNull()
return if (mode == "realtime" && transport == "gateway-relay" && (brain == null || brain == "agent-consult")) {
TalkModeExecutionMode.RealtimeRelay
} else {
TalkModeExecutionMode.Native
}
}
fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
val fallback = TalkDefaults.defaultSilenceTimeoutMs
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback

View File

@@ -157,7 +157,6 @@ class TalkModeManager internal constructor(
private val completedRunStates = LinkedHashMap<String, Boolean>()
private val completedRunTexts = LinkedHashMap<String, String>()
private var configLoaded = false
private var executionMode = TalkModeExecutionMode.Native
private val startGeneration = AtomicLong(0L)
@Volatile private var realtimeSessionId: String? = null
@@ -480,6 +479,19 @@ class TalkModeManager internal constructor(
pendingRunId = null
}
internal suspend fun runE2eRealtimeTurn(
userText: String,
assistantText: String,
timeoutMs: Long,
) {
if (!_isEnabled.value) {
setEnabled(true)
}
val sessionId = awaitRealtimeSessionId(timeoutMs)
handleGatewayEvent("talk.event", realtimeTranscriptPayload(sessionId = sessionId, role = "user", text = userText))
handleGatewayEvent("talk.event", realtimeTranscriptPayload(sessionId = sessionId, role = "assistant", text = assistantText))
}
fun setPlaybackEnabled(enabled: Boolean) {
if (playbackEnabled == enabled) return
playbackEnabled = enabled
@@ -513,53 +525,17 @@ class TalkModeManager internal constructor(
try {
ensureConfigLoaded()
if (generation != startGeneration.get() || !_isEnabled.value || stopRequested) return@launch
if (executionMode == TalkModeExecutionMode.RealtimeRelay) {
startRealtimeRelay(generation)
} else {
startNativeRecognition(generation)
}
startRealtimeRelay(generation)
} catch (err: Throwable) {
if (err is CancellationException) return@launch
_statusText.value = "Start failed: ${err.message ?: err::class.simpleName}"
Log.w(tag, "start failed: ${err.message ?: err::class.simpleName}")
if (executionMode == TalkModeExecutionMode.RealtimeRelay) {
stopRealtimeRelay(closeSession = false, preserveStatus = true)
disableRealtimeModeAndNotifyOwner()
}
stopRealtimeRelay(closeSession = false, preserveStatus = true)
disableRealtimeModeAndNotifyOwner()
}
}
}
private suspend fun startNativeRecognition(generation: Long) {
withContext(Dispatchers.Main) {
if (generation != startGeneration.get()) return@withContext
if (!_isEnabled.value || stopRequested) return@withContext
if (_isListening.value) return@withContext
Log.d(tag, "start native")
if (!SpeechRecognizer.isRecognitionAvailable(context)) {
_statusText.value = "Speech recognizer unavailable"
Log.w(tag, "speech recognizer unavailable")
return@withContext
}
val micOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
if (!micOk) {
_statusText.value = "Microphone permission required"
Log.w(tag, "microphone permission required")
return@withContext
}
recognizer?.destroy()
recognizer = SpeechRecognizer.createSpeechRecognizer(context).also { it.setRecognitionListener(listener) }
startListeningInternal(markListening = true)
startSilenceMonitor()
Log.d(tag, "listening")
}
}
private fun stop() {
stopRequested = true
finalizeInFlight = false
@@ -597,6 +573,19 @@ class TalkModeManager internal constructor(
shutdownTextToSpeech()
}
private suspend fun awaitRealtimeSessionId(timeoutMs: Long): String =
withTimeout(timeoutMs) {
while (true) {
realtimeSessionId?.let { return@withTimeout it }
val status = _statusText.value
if (!_isEnabled.value && status != "Off") {
throw IllegalStateException(status)
}
delay(100L)
}
error("unreachable")
}
private suspend fun startRealtimeRelay(generation: Long) {
if (!isConnected()) {
_statusText.value = "Gateway not connected"
@@ -852,6 +841,19 @@ class TalkModeManager internal constructor(
}
}
private fun realtimeTranscriptPayload(
sessionId: String,
role: String,
text: String,
): String =
buildJsonObject {
put("relaySessionId", JsonPrimitive(sessionId))
put("type", JsonPrimitive("transcript"))
put("role", JsonPrimitive(role))
put("text", JsonPrimitive(text))
put("final", JsonPrimitive(true))
}.toString()
private fun playRealtimeAudio(bytes: ByteArray) {
if (!playbackEnabled || realtimeOutputSuppressed || bytes.isEmpty()) return
val queue = ensureRealtimeAudioQueue()
@@ -2182,11 +2184,9 @@ class TalkModeManager internal constructor(
val parsed = TalkModeGatewayConfigParser.parse(root?.get("config").asObjectOrNull())
silenceWindowMs = parsed.silenceTimeoutMs
parsed.interruptOnSpeech?.let { interruptOnSpeech = it }
executionMode = parsed.executionMode
configLoaded = true
} catch (_: Throwable) {
silenceWindowMs = TalkDefaults.defaultSilenceTimeoutMs
executionMode = TalkModeExecutionMode.Native
configLoaded = false
}
}

View File

@@ -3,6 +3,9 @@
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
tools:node="remove" />

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -265,6 +266,126 @@ class GatewaySessionInvokeTest {
}
}
@Test
fun connect_reusesStoredDeviceTokenScopes() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val connectParams = CompletableDeferred<JsonObject>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, frame ->
if (method == "connect") {
if (!connectParams.isCompleted) {
connectParams.complete(frame["params"]!!.jsonObject)
}
webSocket.send(connectResponseFrame(id))
webSocket.close(1000, "done")
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
harness.deviceAuthStore.saveToken(
deviceId = deviceId,
role = "operator",
token = "operator-device-token",
scopes = listOf("operator.pairing", "operator.write"),
)
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
role = "operator",
scopes = listOf("operator.approvals", "operator.read", "operator.write"),
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val params = withTimeout(TEST_TIMEOUT_MS) { connectParams.await() }
assertEquals(
"operator-device-token",
params["auth"]
?.jsonObject
?.get("token")
?.jsonPrimitive
?.content,
)
assertEquals(listOf("operator.pairing", "operator.write"), params.scopes())
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun bootstrapConnect_filtersOperatorHandoffScopesFromConnectRequest() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val connectParams = CompletableDeferred<JsonObject>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, frame ->
if (method == "connect") {
if (!connectParams.isCompleted) {
connectParams.complete(frame["params"]!!.jsonObject)
}
webSocket.send(connectResponseFrame(id))
webSocket.close(1000, "done")
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
bootstrapToken = "setup-bootstrap-token",
role = "operator",
scopes =
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val params = withTimeout(TEST_TIMEOUT_MS) { connectParams.await() }
assertEquals(
"setup-bootstrap-token",
params["auth"]
?.jsonObject
?.get("bootstrapToken")
?.jsonPrimitive
?.content,
)
assertEquals(
listOf(
"operator.approvals",
"operator.read",
"operator.write",
),
params.scopes(),
)
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun connect_retriesWithStoredDeviceTokenAfterSharedTokenMismatch() =
runBlocking {
@@ -417,10 +538,7 @@ class GatewaySessionInvokeTest {
assertEquals("bootstrap-node-token", nodeEntry?.token)
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(
listOf("operator.approvals", "operator.pairing", "operator.read", "operator.write"),
operatorEntry?.scopes,
)
assertEquals(listOf("operator.approvals", "operator.read", "operator.write"), operatorEntry?.scopes)
} finally {
shutdownHarness(harness, server)
}
@@ -682,6 +800,11 @@ class GatewaySessionInvokeTest {
private fun testJson(): Json = Json { ignoreUnknownKeys = true }
private fun JsonObject.scopes(): List<String> =
(this["scopes"] as? JsonArray)
?.map { it.jsonPrimitive.content }
?: emptyList()
private fun createNodeHarness(
connected: CompletableDeferred<Unit>,
lastDisconnect: AtomicReference<String>,
@@ -714,6 +837,8 @@ class GatewaySessionInvokeTest {
port: Int,
token: String? = "test-token",
bootstrapToken: String? = null,
role: String = "node",
scopes: List<String> = listOf("node:invoke"),
) {
session.connect(
endpoint =
@@ -729,8 +854,8 @@ class GatewaySessionInvokeTest {
password = null,
options =
GatewayConnectOptions(
role = "node",
scopes = listOf("node:invoke"),
role = role,
scopes = scopes,
caps = emptyList(),
commands = emptyList(),
permissions = emptyMap(),
@@ -740,7 +865,7 @@ class GatewaySessionInvokeTest {
displayName = "Android Test",
version = "1.0.0-test",
platform = "android",
mode = "node",
mode = role,
instanceId = "android-test-instance",
deviceFamily = "android",
modelIdentifier = "test",

View File

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

View File

@@ -100,6 +100,40 @@ class MicCaptureManagerTest {
assertEquals(emptyList<VoiceConversationEntry>(), manager.conversation.value)
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun submittedTranscribedMessageUsesGatewayTurnPath() =
runTest {
val sentMessages = mutableListOf<String>()
val manager =
createManager(
scope = this,
sendToGateway = { message, onRunIdKnown ->
sentMessages += message
onRunIdKnown("run-voice-e2e")
"run-voice-e2e"
},
)
manager.onGatewayConnectionChanged(true)
manager.submitTranscribedMessage("voice e2e message")
runCurrent()
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-voice-e2e", text = "voice e2e reply"))
advanceUntilIdle()
assertEquals(listOf("voice e2e message"), sentMessages)
assertEquals(
listOf(VoiceConversationRole.User, VoiceConversationRole.Assistant),
manager.conversation.value.map { it.role },
)
assertEquals(
"voice e2e reply",
manager.conversation.value
.last()
.text,
)
}
@Test
fun pcm16FramesAreEncodedAsPcmuFrames() {
val manager = createManager()

View File

@@ -62,37 +62,4 @@ class TalkModeConfigParsingTest {
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk),
)
}
@Test
fun defaultsToNativeTalkMode() {
val talk =
buildJsonObject {
put("realtime", buildJsonObject { put("transport", "webrtc") })
}
assertEquals(
TalkModeExecutionMode.Native,
TalkModeGatewayConfigParser.resolvedExecutionMode(talk),
)
}
@Test
fun usesRealtimeRelayWhenGatewayRelayIsConfigured() {
val talk =
buildJsonObject {
put(
"realtime",
buildJsonObject {
put("mode", "realtime")
put("transport", "gateway-relay")
put("brain", "agent-consult")
},
)
}
assertEquals(
TalkModeExecutionMode.RealtimeRelay,
TalkModeGatewayConfigParser.resolvedExecutionMode(talk),
)
}
}

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.currentTime
@@ -327,6 +328,28 @@ class TalkModeManagerTest {
assertTrue(entries.none { it.isStreaming })
}
@Test
fun e2eRealtimeTurnUsesRelayTranscriptPath() =
runTest {
val manager = createManager(scope = this)
setPrivateField(manager, "realtimeSessionId", "relay-1")
setMutableStateFlow(manager, "_isEnabled", true)
manager.runE2eRealtimeTurn(
userText = "voice e2e user",
assistantText = "voice e2e assistant",
timeoutMs = 1_000L,
)
val entries = manager.conversation.value
assertEquals(2, entries.size)
assertEquals(VoiceConversationRole.User, entries[0].role)
assertEquals("voice e2e user", entries[0].text)
assertEquals(VoiceConversationRole.Assistant, entries[1].role)
assertEquals("voice e2e assistant", entries[1].text)
assertTrue(entries.none { it.isStreaming })
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun realtimeStartWithoutGatewayTurnsTalkOff() =
@@ -339,7 +362,6 @@ class TalkModeManagerTest {
onStoppedByRelay = { stoppedByRelay.set(true) },
)
setPrivateField(manager, "executionMode", TalkModeExecutionMode.RealtimeRelay)
setPrivateField(manager, "configLoaded", true)
manager.setEnabled(true)
advanceUntilIdle()
@@ -483,6 +505,15 @@ class TalkModeManagerTest {
return field.get(target)
}
@Suppress("UNCHECKED_CAST")
private fun <T> setMutableStateFlow(
target: Any,
name: String,
value: T,
) {
(readPrivateField(target, name) as MutableStateFlow<T>).value = value
}
private fun shouldAppendRealtimeCapturedFrame(
manager: TalkModeManager,
length: Int,

230
apps/android/scripts/voice-e2e.sh Executable file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
ANDROID_DIR="$ROOT_DIR/apps/android"
PACKAGE_NAME="ai.openclaw.app"
RECEIVER="$PACKAGE_NAME/.VoiceE2eReceiver"
RUN_ACTION="ai.openclaw.app.debug.RUN_VOICE_E2E"
OPEN_ACTION="ai.openclaw.app.debug.OPEN_VOICE_E2E"
PORT=18789
HOST="127.0.0.1"
MODE="both"
TRANSCRIPT="Reply exactly: Android voice e2e normal path ok."
REALTIME_ASSISTANT="Android realtime voice e2e relay path ok."
TIMEOUT_MS=60000
INSTALL=1
CONNECT=1
CLEANUP=0
START_GATEWAY=0
usage() {
cat <<'USAGE'
Usage: apps/android/scripts/voice-e2e.sh [options]
Options:
--mode connect|normal|realtime|both
Gateway probe or voice path to test. Default: both.
--transcript TEXT Synthetic user transcript for the voice turn.
--realtime-assistant TEXT Synthetic realtime assistant relay text.
--host HOST Gateway host visible from Android. Default: 127.0.0.1.
--port PORT Gateway port. Default: 18789.
--timeout-ms MS Per-mode timeout. Default: 60000.
--skip-install Reuse the installed debug app.
--no-connect Do not rewrite manual gateway settings.
--start-gateway Start a temporary local gateway with bws_get_secret.
--cleanup Stop voice capture after screenshots.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="$2"
shift 2
;;
--transcript)
TRANSCRIPT="$2"
shift 2
;;
--realtime-assistant)
REALTIME_ASSISTANT="$2"
shift 2
;;
--host)
HOST="$2"
shift 2
;;
--port)
PORT="$2"
shift 2
;;
--timeout-ms)
TIMEOUT_MS="$2"
shift 2
;;
--skip-install)
INSTALL=0
shift
;;
--no-connect)
CONNECT=0
shift
;;
--start-gateway)
START_GATEWAY=1
shift
;;
--cleanup)
CLEANUP=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}"
export ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}"
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="/opt/homebrew/opt/openjdk@17/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
ARTIFACT_DIR="/tmp/openclaw-android-voice-e2e-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$ARTIFACT_DIR"
cleanup_gateway() {
if [[ -n "${GATEWAY_PID:-}" ]]; then
kill "$GATEWAY_PID" >/dev/null 2>&1 || true
fi
}
trap cleanup_gateway EXIT
if ! adb devices -l | awk 'NR > 1 && $2 == "device" { found = 1 } END { exit(found ? 0 : 1) }'; then
echo "no authorized Android device found" >&2
adb devices -l >&2
exit 1
fi
adb reverse "tcp:$PORT" "tcp:$PORT" >/dev/null
if [[ "$START_GATEWAY" -eq 1 ]]; then
if command -v bws_get_secret >/dev/null 2>&1; then
OPENCLAW_OPENAI_API_KEY="$(bws_get_secret OPENCLAW_OPENAI_API_KEY)"
else
OPENCLAW_OPENAI_API_KEY="$(zsh -ic 'bws_get_secret OPENCLAW_OPENAI_API_KEY')"
fi
(
cd "$ROOT_DIR"
OPENAI_API_KEY="$OPENCLAW_OPENAI_API_KEY" \
pnpm openclaw gateway run \
--port "$PORT" \
--auth none \
--bind loopback \
--force \
--allow-unconfigured \
--ws-log compact
) >"$ARTIFACT_DIR/gateway.log" 2>&1 &
GATEWAY_PID=$!
sleep 4
if ! kill -0 "$GATEWAY_PID" >/dev/null 2>&1; then
cat "$ARTIFACT_DIR/gateway.log" >&2
exit 1
fi
unset OPENCLAW_OPENAI_API_KEY
fi
if [[ "$INSTALL" -eq 1 ]]; then
(cd "$ANDROID_DIR" && ./gradlew :app:installPlayDebug)
fi
adb shell pm grant "$PACKAGE_NAME" android.permission.RECORD_AUDIO >/dev/null 2>&1 || true
adb shell am force-stop "$PACKAGE_NAME" >/dev/null
adb shell am start -a "$OPEN_ACTION" -n "$PACKAGE_NAME/.MainActivity" >/dev/null
adb logcat -c
run_mode() {
local test_mode="$1"
local result_name="$ARTIFACT_DIR/result-$test_mode.json"
local screenshot_name="$ARTIFACT_DIR/screen-$test_mode.png"
local transcript_base64
local realtime_assistant_base64
transcript_base64="$(printf '%s' "$TRANSCRIPT" | base64 | tr -d '\n')"
realtime_assistant_base64="$(printf '%s' "$REALTIME_ASSISTANT" | base64 | tr -d '\n')"
adb shell run-as "$PACKAGE_NAME" rm -f cache/voice_e2e_result.json >/dev/null 2>&1 || true
local no_connect_flag=true
if [[ "$CONNECT" -eq 1 ]]; then
no_connect_flag=false
fi
adb shell am broadcast \
-a "$RUN_ACTION" \
-n "$RECEIVER" \
--es mode "$test_mode" \
--ez noConnect "$no_connect_flag" \
--es host "$HOST" \
--ei port "$PORT" \
--ez tls false \
--el timeoutMs "$TIMEOUT_MS" \
--el connectTimeoutMs "$TIMEOUT_MS" \
--es transcriptBase64 "$transcript_base64" \
--es realtimeAssistantBase64 "$realtime_assistant_base64" >/dev/null
local deadline=$((SECONDS + TIMEOUT_MS / 1000 + 20))
local result=""
while [[ "$SECONDS" -lt "$deadline" ]]; do
result="$(adb shell run-as "$PACKAGE_NAME" cat cache/voice_e2e_result.json 2>/dev/null | tr -d '\r' || true)"
if [[ -n "$result" ]]; then
break
fi
sleep 1
done
if [[ -z "$result" ]]; then
echo "voice e2e $test_mode timed out waiting for result" >&2
exit 1
fi
printf '%s\n' "$result" >"$result_name"
adb exec-out screencap -p >"$screenshot_name"
if ! grep -q '"ok":true' "$result_name"; then
echo "voice e2e $test_mode failed: $result" >&2
exit 1
fi
}
case "$MODE" in
both)
run_mode normal
run_mode realtime
;;
normal|dictation)
run_mode normal
;;
realtime|talk)
run_mode realtime
;;
connect)
run_mode connect
;;
*)
echo "unknown mode: $MODE" >&2
exit 2
;;
esac
adb logcat -d -v time |
rg -i 'OpenClaw|TalkMode|MicCapture|AudioRecord|SpeechRecognizer|realtime|talk.session|appendAudio|transcript|Talk failed|Transcription failed|Speech network|VoiceE2E' |
tail -250 >"$ARTIFACT_DIR/logcat.txt" || true
if [[ "$CLEANUP" -eq 1 ]]; then
adb shell am broadcast -a "$RUN_ACTION" -n "$RECEIVER" --es mode stop >/dev/null
fi
echo "$ARTIFACT_DIR"

View File

@@ -1,5 +1,13 @@
# OpenClaw iOS Changelog
## 2026.5.26 - 2026-05-26
Maintenance update for the current OpenClaw release.
## 2026.5.25 - 2026-05-25
Maintenance update for the current OpenClaw release.
## 2026.5.24 - 2026-05-24
Maintenance update for the current OpenClaw release.

View File

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

View File

@@ -75,6 +75,387 @@ struct HomeToolbar: View {
}
}
struct TalkToolbarTray: View {
var brighten: Bool
var tint: Color
var statusText: String
var agentName: String
var micLevel: Double
var isListening: Bool
var isSpeaking: Bool
var isUserSpeechDetected: Bool
var permissionState: TalkGatewayPermissionState
var voiceModeTitle: String
var voiceModeSubtitle: String?
var onEnableTalk: () -> Void
var onStopTalk: () -> Void
@Environment(\.colorSchemeContrast) private var contrast
private var state: TalkToolbarTrayState {
TalkToolbarTrayState(
statusText: self.statusText,
isListening: self.isListening,
isSpeaking: self.isSpeaking,
isUserSpeechDetected: self.isUserSpeechDetected,
permissionState: self.permissionState)
}
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(self.tint.opacity(self.state.iconFillOpacity))
.frame(width: 36, height: 36)
Image(systemName: self.state.systemImage)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(self.state.iconColor(tint: self.tint))
}
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 8) {
Text(self.state.title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
if self.state.showsProgress {
ProgressView()
.controlSize(.mini)
}
}
HStack(spacing: 8) {
TalkWaveformView(
mode: self.state.waveformMode(micLevel: self.micLevel),
tint: self.state.waveformTint(tint: self.tint))
.frame(width: 84, height: 18)
.accessibilityHidden(true)
Text(self.subtitle)
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let voiceModeText = self.voiceModeText {
Text(voiceModeText)
.font(.caption2.weight(.semibold))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer(minLength: 0)
switch self.state.action {
case .enable:
Button(action: self.onEnableTalk) {
Label("Enable Talk", systemImage: "key.fill")
.labelStyle(.titleAndIcon)
}
.font(.caption.weight(.semibold))
.buttonStyle(.borderedProminent)
.controlSize(.small)
case .stop:
Button(action: self.onStopTalk) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .bold))
.frame(width: 28, height: 28)
}
.buttonStyle(.plain)
.background {
Circle()
.fill(Color.black.opacity(self.brighten ? 0.10 : 0.18))
.overlay {
Circle()
.strokeBorder(
.white.opacity(self.contrast == .increased ? 0.42 : 0.16),
lineWidth: self.contrast == .increased ? 1.0 : 0.6)
}
}
.accessibilityLabel("Stop Talk")
case .none:
EmptyView()
}
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(.ultraThinMaterial)
.overlay(alignment: .top) {
Rectangle()
.fill(.white.opacity(self.contrast == .increased ? 0.46 : (self.brighten ? 0.18 : 0.12)))
.frame(height: self.contrast == .increased ? 1.0 : 0.6)
.allowsHitTesting(false)
}
.overlay(alignment: .bottom) {
LinearGradient(
colors: [
self.tint.opacity(self.brighten ? 0.12 : 0.16),
.clear,
],
startPoint: .leading,
endPoint: .trailing)
.frame(height: 1)
.allowsHitTesting(false)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Talk Mode")
.accessibilityValue(self.accessibilityValue)
}
private var accessibilityValue: String {
if let voiceModeText {
return "\(self.state.title), \(self.subtitle), \(voiceModeText)"
}
return "\(self.state.title), \(self.subtitle)"
}
private var voiceModeText: String? {
guard !self.state.prefersPermissionCopy else { return nil }
let title = self.voiceModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty, title != "Not loaded" else { return nil }
let subtitle = (self.voiceModeSubtitle ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return subtitle.isEmpty ? title : "\(title)\(subtitle)"
}
private var subtitle: String {
let trimmedAgent = self.agentName.trimmingCharacters(in: .whitespacesAndNewlines)
if self.state.prefersPermissionCopy {
return "Gateway approval needed"
}
if !trimmedAgent.isEmpty {
return trimmedAgent
}
return "OpenClaw"
}
}
private enum TalkToolbarTrayAction {
case none
case enable
case stop
}
private enum TalkWaveformMode: Equatable {
case level(Double)
case inputSpeech
case speaking
case indeterminate
case still
}
private struct TalkToolbarTrayState: Equatable {
let statusText: String
let isListening: Bool
let isSpeaking: Bool
let isUserSpeechDetected: Bool
let permissionState: TalkGatewayPermissionState
private var normalizedStatus: String {
self.statusText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
var title: String {
switch self.permissionState {
case .missingScope, .requestFailed:
return "Gateway permission required"
case .requestingUpgrade:
return "Requesting approval"
case .upgradeRequested:
return "Approval requested"
default:
break
}
if self.isSpeaking { return "Speaking" }
if self.isListening { return "Listening" }
if self.normalizedStatus.contains("connecting") { return "Connecting" }
if self.normalizedStatus.contains("thinking") { return "Asking OpenClaw" }
if self.normalizedStatus == "ready" { return "Ready to talk" }
if self.normalizedStatus.isEmpty || self.normalizedStatus == "off" { return "Talk" }
return self.statusText
}
var systemImage: String {
switch self.permissionState {
case .missingScope, .requestFailed:
return "key.fill"
case .requestingUpgrade:
return "paperplane.fill"
case .upgradeRequested:
return "hourglass"
default:
break
}
if self.isSpeaking { return "speaker.wave.2.fill" }
if self.isListening { return "mic.fill" }
if self.normalizedStatus.contains("thinking") { return "sparkles" }
if self.normalizedStatus.contains("connecting") { return "dot.radiowaves.left.and.right" }
return "waveform"
}
var action: TalkToolbarTrayAction {
switch self.permissionState {
case .missingScope, .requestFailed:
.enable
case .requestingUpgrade, .upgradeRequested:
.none
default:
.stop
}
}
var showsProgress: Bool {
switch self.permissionState {
case .requestingUpgrade, .upgradeRequested:
true
default:
self.normalizedStatus.contains("connecting") || self.normalizedStatus.contains("thinking")
}
}
var prefersPermissionCopy: Bool {
switch self.permissionState {
case .missingScope, .requestingUpgrade, .upgradeRequested, .requestFailed:
true
default:
false
}
}
var iconFillOpacity: Double {
self.prefersPermissionCopy ? 0.18 : 0.24
}
func iconColor(tint: Color) -> Color {
switch self.permissionState {
case .requestFailed:
.red
case .missingScope, .requestingUpgrade, .upgradeRequested:
.orange
default:
tint
}
}
func waveformTint(tint: Color) -> Color {
switch self.permissionState {
case .requestFailed:
.red
case .missingScope, .requestingUpgrade, .upgradeRequested:
.orange
default:
tint
}
}
func waveformMode(micLevel: Double) -> TalkWaveformMode {
switch self.permissionState {
case .requestingUpgrade, .upgradeRequested:
return .indeterminate
case .missingScope, .requestFailed:
return .still
default:
break
}
if self.isSpeaking {
return .speaking
}
if self.isListening, self.isUserSpeechDetected {
return .inputSpeech
}
if self.isListening {
return .level(micLevel)
}
if self.normalizedStatus.contains("connecting") || self.normalizedStatus.contains("thinking") {
return .indeterminate
}
return .still
}
}
private struct TalkWaveformView: View {
var mode: TalkWaveformMode
var tint: Color
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private let barCount = 14
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 24.0)) { timeline in
HStack(alignment: .center, spacing: 3) {
ForEach(0..<self.barCount, id: \.self) { index in
Capsule(style: .continuous)
.fill(self.tint.opacity(self.opacity(for: index)))
.frame(width: 3, height: self.height(for: index, date: timeline.date))
}
}
.frame(maxHeight: .infinity)
}
}
private func height(for index: Int, date: Date) -> CGFloat {
let minimum: Double = 4
let maximum: Double = 18
let amplitude = self.amplitude(for: index, date: date)
return CGFloat(minimum + ((maximum - minimum) * amplitude))
}
private func opacity(for index: Int) -> Double {
switch self.mode {
case .still:
index == self.barCount / 2 ? 0.64 : 0.32
default:
0.78
}
}
private func amplitude(for index: Int, date: Date) -> Double {
if self.reduceMotion {
switch self.mode {
case let .level(level):
return min(max(level, 0.10), 1.0)
case .inputSpeech:
return 0.72
case .speaking:
return 0.62
case .indeterminate:
return 0.34
case .still:
return 0.18
}
}
let t = date.timeIntervalSinceReferenceDate
let phase = Double(index) * 0.52
switch self.mode {
case let .level(level):
let clamped = min(max(level, 0), 1)
let shaped = 0.12 + (0.88 * clamped)
let variation = 0.72 + (0.28 * sin((t * 12.0) + phase))
return min(max(shaped * variation, 0.10), 1.0)
case .inputSpeech:
let primary = 0.5 + (0.5 * sin((t * 14.0) + phase))
let secondary = 0.5 + (0.5 * sin((t * 5.0) + (phase * 1.35)))
return min(max(0.16 + (0.60 * primary) + (0.24 * secondary), 0.14), 1.0)
case .speaking:
let wave = 0.5 + (0.5 * sin((t * 7.5) + phase))
let secondary = 0.5 + (0.5 * sin((t * 3.0) + (phase * 0.7)))
return min(max(0.18 + (0.58 * wave) + (0.24 * secondary), 0.12), 1.0)
case .indeterminate:
let center = (sin((t * 3.2) + phase) + 1) / 2
return 0.16 + (0.42 * center)
case .still:
return index == self.barCount / 2 ? 0.32 : 0.16
}
}
}
private struct HomeToolbarStatusButton: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.accessibilityReduceMotion) private var reduceMotion

View File

@@ -148,6 +148,8 @@ final class NodeAppModel {
private let operatorGateway = GatewayNodeSession()
private var nodeGatewayTask: Task<Void, Never>?
private var operatorGatewayTask: Task<Void, Never>?
private var forceOperatorTalkPermissionUpgradeRequest = false
private var lastTalkPermissionReconnectAttemptAt: Date?
private var voiceWakeSyncTask: Task<Void, Never>?
@ObservationIgnored private var cameraHUDDismissTask: Task<Void, Never>?
@ObservationIgnored private lazy var capabilityRouter: NodeCapabilityRouter = self.buildCapabilityRouter()
@@ -609,6 +611,85 @@ final class NodeAppModel {
self.talkMode.applyProviderSelectionChanged()
}
func requestTalkPermissionUpgrade() {
guard let config = self.activeGatewayConnectConfig else {
self.talkMode.gatewayTalkPermissionState = .requestFailed("Gateway is not connected")
self.talkMode.statusText = "Gateway not connected"
return
}
GatewayDiagnostics.log("talk permission upgrade requested")
self.talkMode.gatewayTalkPermissionState = .requestingUpgrade
self.talkMode.statusText = "Requesting Talk approval"
self.forceOperatorTalkPermissionUpgradeRequest = true
self.gatewayAutoReconnectEnabled = true
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.lastGatewayProblem = nil
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
let sessionBox = config.tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) }
Task { [weak self] in
guard let self else { return }
await self.operatorGateway.disconnect()
await MainActor.run {
self.startOperatorGatewayLoop(
url: config.url,
stableID: config.effectiveStableID,
token: config.token,
bootstrapToken: config.bootstrapToken,
password: config.password,
nodeOptions: config.nodeOptions,
sessionBox: sessionBox)
}
}
}
func pollTalkPermissionUpgrade() async {
guard self.talkMode.gatewayTalkPermissionState.isApprovalRequestInProgress else {
await self.talkMode.reloadConfig()
await self.talkMode.prefetchRealtimeSessionIfReady(reason: "talk_permission_poll")
return
}
guard let cfg = self.activeGatewayConnectConfig else {
self.talkMode.gatewayTalkPermissionState = .requestFailed("Gateway is not connected")
self.talkMode.statusText = "Gateway not connected"
return
}
let now = Date()
if let lastTalkPermissionReconnectAttemptAt,
now.timeIntervalSince(lastTalkPermissionReconnectAttemptAt) < 6
{
return
}
self.lastTalkPermissionReconnectAttemptAt = now
GatewayDiagnostics.log("talk permission approval poll reconnect")
self.gatewayAutoReconnectEnabled = true
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.ensureOperatorReconnectLoopIfNeeded()
if self.operatorGatewayTask == nil {
let sessionBox = cfg.tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) }
self.startOperatorGatewayLoop(
url: cfg.url,
stableID: cfg.effectiveStableID,
token: cfg.token,
bootstrapToken: cfg.bootstrapToken,
password: cfg.password,
nodeOptions: cfg.nodeOptions,
sessionBox: sessionBox)
}
guard await self.waitForOperatorConnection(timeoutMs: 2500, pollMs: 250) else {
return
}
await self.talkMode.reloadConfig()
await self.talkMode.prefetchRealtimeSessionIfReady(reason: "talk_permission_poll_connected")
}
func requestLocationPermissions(mode: OpenClawLocationMode) async -> Bool {
guard mode != .off else { return true }
let status = await self.locationService.ensureAuthorization(mode: mode)
@@ -2026,7 +2107,11 @@ extension NodeAppModel {
sessionBox: WebSocketSessionBox?) async
{
self.clearPersistedGatewayBootstrapTokenIfNeeded()
if self.operatorGatewayTask == nil, self.shouldStartOperatorGatewayLoop(
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
if self.shouldStartOperatorGatewayLoop(
token: token,
bootstrapToken: nil,
password: password,
@@ -2107,7 +2192,8 @@ extension NodeAppModel {
displayName: nodeOptions.clientDisplayName,
includeApprovalScope: self.shouldRequestOperatorApprovalScope(
token: reconnectAuth.token,
password: reconnectAuth.password))
password: reconnectAuth.password),
forceExplicitScopes: self.forceOperatorTalkPermissionUpgradeRequest)
do {
try await self.operatorGateway.connect(
@@ -2121,11 +2207,13 @@ extension NodeAppModel {
guard let self else { return }
await MainActor.run {
self.operatorConnected = true
self.forceOperatorTalkPermissionUpgradeRequest = false
self.talkMode.updateGatewayConnected(true)
}
GatewayDiagnostics.log(
"operator gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")")
await self.talkMode.reloadConfig()
await self.talkMode.prefetchRealtimeSessionIfReady(reason: "operator_connected")
await self.refreshBrandingFromGateway()
await self.refreshAgentsFromGateway()
await self.refreshShareRouteFromGateway()
@@ -2158,6 +2246,29 @@ extension NodeAppModel {
} catch {
attempt += 1
GatewayDiagnostics.log("operator gateway connect error: \(error.localizedDescription)")
let problem = await MainActor.run {
let nextProblem = GatewayConnectionProblemMapper.map(error: error)
if let nextProblem {
if nextProblem.kind == .pairingScopeUpgradeRequired {
self.gatewayPairingPaused = true
self.gatewayPairingRequestId = nextProblem.requestId
self.talkMode.markTalkPermissionUpgradeRequested(requestId: nextProblem.requestId)
}
}
return nextProblem
}
if problem?.needsPairingApproval == true {
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
break
}
if problem?.pauseReconnect == true {
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
break
}
let sleepSeconds = min(8.0, 0.5 * pow(1.7, Double(attempt)))
try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000))
}
@@ -2420,7 +2531,8 @@ extension NodeAppModel {
private func makeOperatorConnectOptions(
clientId: String,
displayName: String?,
includeApprovalScope: Bool) -> GatewayConnectOptions
includeApprovalScope: Bool,
forceExplicitScopes: Bool = false) -> GatewayConnectOptions
{
var scopes = ["operator.read", "operator.write", "operator.talk.secrets"]
// Preserve reconnect compatibility for older paired operator tokens that were
@@ -2431,6 +2543,7 @@ extension NodeAppModel {
return GatewayConnectOptions(
role: "operator",
scopes: scopes,
scopesAreExplicit: forceExplicitScopes,
caps: [],
commands: [],
permissions: [:],
@@ -2561,7 +2674,9 @@ extension NodeAppModel {
func reloadTalkConfig() {
Task { [weak self] in
await self?.talkMode.reloadConfig()
guard let self else { return }
await self.talkMode.reloadConfig()
await self.talkMode.prefetchRealtimeSessionIfReady(reason: "config_reload")
}
}
@@ -4283,12 +4398,14 @@ extension NodeAppModel {
func _test_makeOperatorConnectOptions(
clientId: String,
displayName: String?,
includeApprovalScope: Bool) -> GatewayConnectOptions
includeApprovalScope: Bool,
forceExplicitScopes: Bool = false) -> GatewayConnectOptions
{
self.makeOperatorConnectOptions(
clientId: clientId,
displayName: displayName,
includeApprovalScope: includeApprovalScope)
includeApprovalScope: includeApprovalScope,
forceExplicitScopes: forceExplicitScopes)
}
func _test_presentExecApprovalPrompt(_ prompt: ExecApprovalPrompt) {

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