Compare commits

...

264 Commits

Author SHA1 Message Date
Vincent Koc
c642e1da24 fix(gateway): guard node approval policy writes 2026-06-01 01:52:02 +01:00
Ted Li
6316648bab fix(openai): keep stop-finished tool calls
Preserve silent structured OpenAI-compatible tool calls when providers stream tool_calls but finish with finish_reason stop, while keeping visible-text stop responses and unfinished streams from executing spurious tool calls.

Fixes #88791.

Verification:
- pnpm tsgo:prod
- node scripts/run-vitest.mjs src/llm/providers/openai-completions.test.ts src/agents/openai-transport-stream.test.ts
- loopback OpenAI-compatible SSE proof against createOpenAICompletionsTransportStreamFn
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
2026-05-31 19:41:23 -04:00
Gio Della-Libera
bf777b9af2 fix(doctor): quiet tool policy audits during probes
Keep runtime tool-policy removal audits at the normal info level, but lower diagnostic-only doctor tool-schema probes to debug so expected profile filtering does not clutter normal doctor output.

Also updates current-base test expectations for the Talk custom select and a promise-executor lint rule so the PR remains green on the latest base.

Fixes #87798.

Proof:
- CI https://github.com/openclaw/openclaw/actions/runs/26727664397
- Real behavior proof https://github.com/openclaw/openclaw/actions/runs/26727667473
- Local focused Vitest, broad lint, touched-file format/lint, and autoreview clean.

Co-authored-by: Gio Della-Libera <40915808+giodl73-repo@users.noreply.github.com>
2026-05-31 19:37:13 -04:00
Peter Steinberger
fba9eac7eb fix(google): register Vertex static catalog rows 2026-06-01 00:36:31 +01:00
Peter Steinberger
5965522af5 fix(copilot): preserve Claude 1M capabilities 2026-06-01 00:36:31 +01:00
Peter Steinberger
f18fd2094f fix(agents): match provider-scoped context ids 2026-06-01 00:36:30 +01:00
Peter Steinberger
770ee8eba6 fix(models): refresh provider catalog metadata 2026-06-01 00:36:30 +01:00
Vincent Koc
b891d42f3a refactor: share talk session turn handling 2026-06-01 01:32:06 +02:00
Vincent Koc
705bdcec70 fix(gateway): harden MCP loopback tool schemas 2026-05-31 19:30:02 -04:00
github-actions[bot]
db7aff8843 chore(ui): refresh fa control ui locale 2026-05-31 23:23:45 +00:00
github-actions[bot]
d30329fb0e chore(ui): refresh nl control ui locale 2026-05-31 23:23:37 +00:00
github-actions[bot]
c7f3d60722 chore(ui): refresh vi control ui locale 2026-05-31 23:23:13 +00:00
github-actions[bot]
0ffaeb1273 chore(ui): refresh th control ui locale 2026-05-31 23:23:06 +00:00
github-actions[bot]
c43a571170 chore(ui): refresh pl control ui locale 2026-05-31 23:23:02 +00:00
github-actions[bot]
dd8b9bdcb8 chore(ui): refresh id control ui locale 2026-05-31 23:22:48 +00:00
github-actions[bot]
399f55e511 chore(ui): refresh uk control ui locale 2026-05-31 23:22:29 +00:00
github-actions[bot]
7e654b40b8 chore(ui): refresh tr control ui locale 2026-05-31 23:22:18 +00:00
github-actions[bot]
7b119ec60d chore(ui): refresh it control ui locale 2026-05-31 23:22:15 +00:00
github-actions[bot]
c1fffe1074 chore(ui): refresh ar control ui locale 2026-05-31 23:22:05 +00:00
github-actions[bot]
530f3aaab7 chore(ui): refresh fr control ui locale 2026-05-31 23:21:43 +00:00
github-actions[bot]
3ec1a25de4 chore(ui): refresh ja-JP control ui locale 2026-05-31 23:21:30 +00:00
github-actions[bot]
5a6ec67eb0 chore(ui): refresh es control ui locale 2026-05-31 23:21:27 +00:00
github-actions[bot]
0fdca6974d chore(ui): refresh ko control ui locale 2026-05-31 23:21:24 +00:00
Jerry-Xin
dc344a33fb fix(cron): retire MCP runtimes on isolated cron cleanup
Retire isolated cron session MCP runtimes on timeout and dispose so orphaned MCP servers do not accumulate after cron cleanup. Bound MCP session disposal to 5 seconds and force-close hung transports, including streamable-HTTP DELETE hangs, to prefer gateway availability over unbounded teardown.

Fixes #87821.
PR: #87981.
Proof: latest Real behavior proof check passed after body fix; local autoreview clean with focused cron/gateway/MCP tests covering 108 tests.

Co-authored-by: 忻役 <xinyi@mininglamp.com>
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
2026-06-01 00:21:14 +01:00
github-actions[bot]
e4a766f2f4 chore(ui): refresh zh-TW control ui locale 2026-05-31 23:20:54 +00:00
github-actions[bot]
ad07ba141d chore(ui): refresh pt-BR control ui locale 2026-05-31 23:20:46 +00:00
github-actions[bot]
bd78737f94 chore(ui): refresh de control ui locale 2026-05-31 23:20:41 +00:00
github-actions[bot]
5f6e608c60 chore(ui): refresh zh-CN control ui locale 2026-05-31 23:20:37 +00:00
Vincent Koc
ddbd16a04a fix(ui): honor chromium executable override 2026-06-01 00:20:10 +01:00
Vincent Koc
03151a2ebe test(release): repair stale e2e mocks 2026-06-01 00:20:10 +01:00
Vincent Koc
1b69e7a005 fix(plugin-sdk): keep llm core alias on source graph 2026-06-01 00:20:10 +01:00
Vincent Koc
227530f906 test(imessage): align service-qualified target expectations 2026-06-01 00:20:10 +01:00
Vincent Koc
6df3fd5730 fix(gateway): list commands from gateway plugin registry 2026-06-01 00:20:10 +01:00
Vincent Koc
7c315252d6 test(whatsapp): wait on inbox delivery in monitor helper 2026-06-01 00:20:10 +01:00
Vincent Koc
0d7abcc94f test(telegram): exercise blocked spooled timeout lane 2026-06-01 00:20:09 +01:00
Vincent Koc
344773ba09 fix(openrouter): cap music stream request timeouts 2026-06-01 00:20:09 +01:00
Vincent Koc
ae4550f48b test(qa-lab): preserve cleanup phase labels 2026-06-01 00:20:09 +01:00
Vincent Koc
fdd02444b7 ci: add ARM Testbox lane 2026-06-01 00:20:09 +01:00
Peter Steinberger
3491834d49 Migrate iMessage monitor state to SQLite (#88797)
* refactor: move imessage monitor state to sqlite

* test: use OpenClaw temp root in iMessage state helper

* test: avoid pending promise lint in chat tests

* test: harden gateway ci flakes

* test: align session list merge expectation
2026-06-01 00:19:51 +01:00
Vincent Koc
12cf34a8ea refactor: share send inflight helpers 2026-06-01 01:18:38 +02:00
Peter Steinberger
d328a0d7a0 feat: calm chat composer controls 2026-06-01 00:18:04 +01:00
colmbrogan
421ad93203 fix(imessage): tolerate self-chat timestamp skew
Fixes iMessage self-chat reflection dedupe when reflected rows arrive with sub-second `created_at` skew, while keeping ambiguous normal-DM suppression exact-match only.

Maintainer follow-ups scoped skew tolerance to confirmed self-chat remembered rows and bounded cache cleanup so TTL-only expiry cannot leave the insertion-order queue growing indefinitely.

Verification:
- `node scripts/run-vitest.mjs extensions/imessage/src/monitor/self-chat-cache.test.ts extensions/imessage/src/monitor/self-chat-dedupe.test.ts extensions/imessage/src/monitor/inbound-processing.test.ts`
- `pnpm oxlint extensions/imessage/src/monitor/self-chat-cache.ts extensions/imessage/src/monitor/self-chat-cache.test.ts extensions/imessage/src/monitor/self-chat-dedupe.test.ts`
- `git diff --check origin/main...HEAD`
- autoreview clean on branch tip
- CI run 26727192244 green; Real behavior proof run 26727196218 green

Co-authored-by: Colm O Brogain <73212305+colmbrogan@users.noreply.github.com>
2026-06-01 00:14:47 +01:00
Vincent Koc
dc05f598bb fix(doctor): report runtime tool schema errors 2026-06-01 00:14:36 +01:00
Alix-007
3171278372 fix(gateway): hide phantom agent store rows from sessions.list
Fixes #57376.

Hide placeholder agent store keys from sessions.list while preserving real agent-scoped sessions.

Co-authored-by: Alix-007 <li.long15@xydigit.com>
2026-06-01 00:14:09 +01:00
Feelw00
01193dea26 fix: make task persistence failures explicit
Preserve task and TaskFlow durability by persisting before in-memory registry mutation and surfacing explicit persistence failures instead of reporting fake success.

Adds non-throwing try-create runtime helpers while keeping existing throwing public create APIs compatible. Maintainer follow-up keeps task/TaskFlow sync repair bounded, prevents split task/delivery-state writes, and keeps CI green on the current base.

Thanks @Feelw00.
2026-06-01 00:12:28 +01:00
Coder
cb9847968a fix(subagents): roll token usage formatters over to m
Roll both subagent token usage formatters over to the million unit when rounded thousands reach the next unit.

The original fix covers `formatTokenShort`, which feeds the subagent list usage line. The maintainer follow-up applies the same unit-boundary rule to compact subagent announcement stats, preserving that formatter's one-decimal style while preventing `1000.0k` output.

Verification:
- focused runtime probe for list and compact announce stats at 999,999 tokens
- `oxfmt --check` on touched formatter/test files
- `git diff --check origin/main..HEAD`
- `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-pr88209.tsbuildinfo`
- autoreview local closeout clean
- exact-head CI passed for Real behavior proof, check-test-types, check-prod-types, check-guards, security-fast, and preflight

Known unrelated current-main reds at merge: `check-lint`, `checks-node-agentic-gateway-methods`, and `checks-node-agentic-control-plane-agent-chat`.

Co-authored-by: coder999999999 <coder999999999@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:07:45 +01:00
Vincent Koc
54987715f3 fix(ci): repair main lint and gateway session tests 2026-06-01 00:05:41 +01:00
Silvester
0c74f18a1c fix(microsoft-foundry): skip DeepSeek V4 thinking params on Foundry fallback
Skip the generic DeepSeek V4 OpenAI-compatible `thinking` payload wrapper for Microsoft Foundry fallback models. Foundry's OpenAI-compatible gateway rejects the non-standard top-level `thinking` argument, while the rest of the DeepSeek proxy path still keeps the wrapper.

Proof:
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/agents/embedded-agent-runner-extraparams.test.ts"
- node scripts/run-vitest.mjs src/agents/embedded-agent-runner-extraparams.test.ts passed, 130/130
- CI run 26681069909 passed for c950ac112e

Thanks @silvesterxm.
2026-06-01 00:03:32 +01:00
Vincent Koc
59122812c0 refactor: share agent id resolver 2026-06-01 01:03:07 +02:00
Alix-007
bc95af1b7c fix(memory-core): stop dream diary fallback leaks
Stop memory-core dream diary fallback paths from persisting raw memory staging snippets or promotions into DREAMS.md when narrative generation times out, returns empty output, or fails in request-scoped runtime. Successful generated narratives are unchanged.

Maintainer fixup: align current gateway session-list tests with the full loadSessionEntry mock shape and model-derived context token behavior on main.

Fixes #88391

Co-authored-by: Alix-007 <li.long15@xydigit.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:00:22 +01:00
yozakura-ava
144405e562 fix(agents): cap bootstrap snapshot cache
Cap the in-memory workspace bootstrap snapshot cache to 64 session keys so long-lived gateway processes do not retain one loaded bundle per distinct session key indefinitely. Older entries are evicted while active keys continue refreshing against the guarded workspace loader.

Verification:
- node scripts/run-vitest.mjs src/agents/bootstrap-cache.test.ts

PR: #88149
2026-05-31 23:56:47 +01:00
Vincent Koc
290b19275b refactor: share cron request helpers 2026-06-01 00:53:00 +02:00
Rain
72f74b33e1 fix(agents): guard transport payload sanitizer against non-string input
sanitizeTransportPayloadText() called text.replace() directly, so runtime-undefined content from malformed replay/error handling could crash embedded agent transport serialization with "Cannot read properties of undefined (reading 'replace')".

Return an empty string for non-string runtime payloads at the shared sanitizer boundary, preserving existing unpaired-surrogate cleanup for strings. Empty values still degrade through sanitizeNonEmptyTransportPayloadText() to "(no output)" where that non-empty fallback is required.

Proof:
- pnpm test src/agents/transport-stream-shared.test.ts
- pnpm exec oxfmt --check --threads=1 src/agents/transport-stream-shared.ts src/agents/transport-stream-shared.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "pnpm test src/agents/transport-stream-shared.test.ts"

Fixes #60113

Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 23:45:39 +01:00
Vincent Koc
bb673f47b2 refactor: share agent run snapshot shape 2026-06-01 00:39:13 +02:00
Vincent Koc
16ef9c1435 fix(ci): use reliable Azure Crabbox capacity 2026-06-01 00:38:11 +02:00
Peter Steinberger
2b30951b80 feat: calm composer controls (#88772) 2026-05-31 23:37:27 +01:00
Peter Steinberger
56b8030cd9 fix(qa-lab): avoid returning timer from promise executor 2026-05-31 23:34:13 +01:00
DocNR
5706619068 fix(nostr): decode npub allowFrom entries to hex correctly
Fix Nostr allowFrom npub normalization by returning the decoded hex string from nostr-tools instead of iterating the hex string as bytes.

Proof:
- node scripts/run-vitest.mjs extensions/nostr/src/nostr-bus.test.ts
- PR CI green at head 7c3433435b

Co-authored-by: DocNR <danieljwyler@gmail.com>
2026-05-31 23:33:45 +01:00
Vincent Koc
edc0a22179 fix(agents): quarantine tools before schema normalization 2026-05-31 23:33:03 +01:00
Peter Steinberger
2682c02774 perf: hydrate chat history session metadata
Use chat.history metadata to hydrate TUI and web startup state without the extra sessions.list refresh, with guards for aliases, stale active rows, blank-session defaults, and lightweight TUI usage metadata.
2026-05-31 23:31:15 +01:00
Vincent Koc
59683978e1 refactor: share voice-call config extraction 2026-06-01 00:19:33 +02:00
Peter Steinberger
c8f8907f15 fix(feishu): guard webhook readiness fetch 2026-05-31 23:18:09 +01:00
Vincent Koc
8eb1838dfa refactor: share web login unavailable response 2026-06-01 00:13:34 +02:00
Jason O'Neal
01f6ad6056 fix: suppress raw provider errors in channel delivery
Fixes #69737.

Suppresses raw and raw-derived provider error text at the user-facing assistant lifecycle and reply-payload boundaries, including structured provider payloads, escaped JSON payloads, and aborted turns carrying provider failures. Keeps safe schema rejection and rate-limit guidance while preserving internal diagnostics.

Proof:
- OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run src/agents/embedded-agent-helpers.formatassistanterrortext.test.ts src/agents/embedded-agent-runner/run/payloads.errors.test.ts src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts src/agents/embedded-agent-helpers/errors.test.ts
- oxfmt --check on touched files
- git diff --check origin/main...HEAD
- autoreview --mode branch --base origin/main clean
- GitHub exact-head checks green on b46e197f62

Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
2026-05-31 23:10:46 +01:00
Peter Steinberger
b7f657b3b0 chore(lint): fix app render promise executor 2026-05-31 23:10:00 +01:00
Peter Steinberger
22cb7fb6b7 chore(lint): enable no-promise-executor-return 2026-05-31 23:06:13 +01:00
Vincent Koc
48afba96a3 refactor: share agents handler helpers 2026-06-01 00:02:22 +02:00
github-actions[bot]
470a1ae8d1 chore(ui): refresh nl control ui locale 2026-05-31 21:59:56 +00:00
github-actions[bot]
a2acfc5049 chore(ui): refresh fa control ui locale 2026-05-31 21:59:48 +00:00
github-actions[bot]
fe8c781d67 chore(ui): refresh vi control ui locale 2026-05-31 21:59:18 +00:00
github-actions[bot]
ac2484f23e chore(ui): refresh pl control ui locale 2026-05-31 21:59:13 +00:00
github-actions[bot]
cabfbdfe0d chore(ui): refresh id control ui locale 2026-05-31 21:59:11 +00:00
github-actions[bot]
5e2472567a chore(ui): refresh th control ui locale 2026-05-31 21:59:07 +00:00
github-actions[bot]
79c4ac73d7 chore(ui): refresh tr control ui locale 2026-05-31 21:58:33 +00:00
github-actions[bot]
2a1882ebcc chore(ui): refresh uk control ui locale 2026-05-31 21:58:31 +00:00
github-actions[bot]
3bb04b67e9 chore(ui): refresh it control ui locale 2026-05-31 21:58:25 +00:00
github-actions[bot]
cd0a7b10e2 chore(ui): refresh ar control ui locale 2026-05-31 21:58:21 +00:00
github-actions[bot]
bc45c36dbc chore(ui): refresh fr control ui locale 2026-05-31 21:57:51 +00:00
github-actions[bot]
7184522fae chore(ui): refresh ko control ui locale 2026-05-31 21:57:45 +00:00
github-actions[bot]
aa74d93aff chore(ui): refresh es control ui locale 2026-05-31 21:57:40 +00:00
github-actions[bot]
be0d3489a6 chore(ui): refresh ja-JP control ui locale 2026-05-31 21:57:38 +00:00
github-actions[bot]
f06b4b9aab chore(ui): refresh pt-BR control ui locale 2026-05-31 21:57:10 +00:00
github-actions[bot]
0700f13d62 chore(ui): refresh zh-TW control ui locale 2026-05-31 21:57:05 +00:00
github-actions[bot]
3c6c247e0a chore(ui): refresh de control ui locale 2026-05-31 21:57:01 +00:00
github-actions[bot]
2e42b1372e chore(ui): refresh zh-CN control ui locale 2026-05-31 21:56:58 +00:00
Shakker
f78bb34cb4 fix: translate Skill Workshop locale strings 2026-05-31 22:55:03 +01:00
Shakker
85c7490f72 fix: refresh Skill Workshop i18n outputs 2026-05-31 22:55:03 +01:00
Shakker
63d93db867 fix: refresh Skill Workshop protocol models 2026-05-31 22:55:03 +01:00
Shakker
2976db4b2c fix: address Skill Workshop UI check failures 2026-05-31 22:55:03 +01:00
Shakker
025bb01268 fix: constrain Skill Workshop navigation 2026-05-31 22:55:03 +01:00
Shakker
7a292bb16e fix: improve Skill Workshop empty states 2026-05-31 22:55:03 +01:00
Shakker
a9e3eade5d fix: tighten Skill Workshop today actions 2026-05-31 22:55:03 +01:00
Shakker
3733cd8d63 fix: clarify Skill Workshop proposal preview 2026-05-31 22:55:03 +01:00
Shakker
190f935b53 fix: address Skill Workshop review findings 2026-05-31 22:55:03 +01:00
Shakker
c21e16c73d fix: add Skill Workshop empty state 2026-05-31 22:55:03 +01:00
Shakker
d52f1ea5ec fix: tighten Skill Workshop today actions 2026-05-31 22:55:03 +01:00
Shakker
13967e17e6 fix: distinguish created Skill Workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
7ad2aa44dd fix: show assistant name in Skill Workshop 2026-05-31 22:55:03 +01:00
Shakker
874b3f921e fix: polish Skill Workshop revision handoff 2026-05-31 22:55:03 +01:00
Shakker
c11d5d6d65 feat: stage Skill Workshop revision handoff 2026-05-31 22:55:03 +01:00
Shakker
11631bf044 feat: animate Skill Workshop chat landing 2026-05-31 22:55:03 +01:00
Shakker
561e993282 fix: stabilize Skill Workshop revise handoff 2026-05-31 22:55:03 +01:00
Shakker
23bf48e69e feat: add reusable Control UI tooltip 2026-05-31 22:55:03 +01:00
Shakker
7d65ea3513 feat: style Skill Workshop revision controls 2026-05-31 22:55:03 +01:00
Shakker
bfac12a184 feat: route Skill Workshop revisions through reusable sessions 2026-05-31 22:55:03 +01:00
Shakker
cdcc151145 feat: attach agent session origin to workshop tool 2026-05-31 22:55:03 +01:00
Shakker
7681b95199 feat: persist Skill Workshop proposal origin 2026-05-31 22:55:03 +01:00
Shakker
caa08a6dc0 feat: show real Skill Workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
4339d7c1d8 feat: add Skill Workshop revision dialog 2026-05-31 22:55:03 +01:00
Shakker
aa187c6496 feat: add Skill Workshop today view 2026-05-31 22:55:03 +01:00
Shakker
34010894c1 feat: preview Skill Workshop actions 2026-05-31 22:55:03 +01:00
Shakker
c74bb4475a feat: resize Skill Workshop proposal list 2026-05-31 22:55:03 +01:00
Shakker
299a023bd1 fix: track reviewed workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
0c852036c7 fix: refine Skill Workshop action bar 2026-05-31 22:55:03 +01:00
Shakker
9cc759dd37 fix: hide Skill Workshop actions after pending 2026-05-31 22:55:03 +01:00
Shakker
d1378650bb fix: keep file preview row focus clean 2026-05-31 22:55:03 +01:00
Shakker
40f99e474a fix: keep file preview keyboard focus modal 2026-05-31 22:55:03 +01:00
Shakker
dc71b5867e fix: align live tool stream labels 2026-05-31 22:55:03 +01:00
Shakker
fd2c65f59b refactor: extract file preview modal component 2026-05-31 22:55:03 +01:00
Shakker
575f74293e feat: search Skill Workshop preview files 2026-05-31 22:55:03 +01:00
Shakker
b27ae3f6e7 fix: remove Skill Workshop modal search focus chrome 2026-05-31 22:55:03 +01:00
Shakker
b388d3dc71 style: add Skill Workshop file preview modal 2026-05-31 22:55:03 +01:00
Shakker
01b7ef9e88 feat: add Skill Workshop file preview modal 2026-05-31 22:55:03 +01:00
Shakker
4b89def277 fix: align Skill Workshop pane surface 2026-05-31 22:55:03 +01:00
Shakker
fabd9469cd fix: tighten Skill Workshop page spacing 2026-05-31 22:55:03 +01:00
Shakker
d3025b4007 fix: resolve Control UI public assets from base path 2026-05-31 22:55:03 +01:00
Shakker
c06096eabc fix: keep Control UI logo root-relative 2026-05-31 22:55:03 +01:00
Shakker
9577e0be5a feat: style Skill Workshop UI 2026-05-31 22:55:03 +01:00
Shakker
b12724b79b feat: add Skill Workshop demo view 2026-05-31 22:55:03 +01:00
Shakker
0de60cec12 feat: add Skill Workshop navigation tab 2026-05-31 22:55:03 +01:00
Vincent Koc
c6232347dc refactor: share exec approvals node invoke 2026-05-31 23:50:30 +02:00
xin zhuang
b73e135f97 fix: resolve google provider default API to google-generative-ai (#88480) (#88512)
When a configured Google provider/model row had no explicit
but had a baseUrl set, the fallback defaulted to openai-completions,
causing Gemini requests to route through the OpenAI Responses
transport instead of the native @google/genai transport.

Made resolveConfiguredProviderDefaultApi provider-aware: for the
google provider, the default API is now google-generative-ai.

Root cause: the generic fallback assumed any provider with a baseUrl
should use openai-completions, which is incorrect for Google's native
Gemini API.

Co-authored-by: xin <1052326311+xin@users.noreply.github.com>
2026-05-31 22:48:48 +01:00
github-actions[bot]
9b6c981260 chore(ui): refresh fa control ui locale 2026-05-31 21:46:04 +00:00
github-actions[bot]
02ac0ec48b chore(ui): refresh nl control ui locale 2026-05-31 21:46:00 +00:00
github-actions[bot]
d8329dedf6 chore(ui): refresh pl control ui locale 2026-05-31 21:45:39 +00:00
github-actions[bot]
b86e8bf359 chore(ui): refresh id control ui locale 2026-05-31 21:45:30 +00:00
github-actions[bot]
3bb9224836 chore(ui): refresh vi control ui locale 2026-05-31 21:45:24 +00:00
github-actions[bot]
fdc10a64e9 chore(ui): refresh th control ui locale 2026-05-31 21:45:20 +00:00
github-actions[bot]
87174c80b6 chore(ui): refresh uk control ui locale 2026-05-31 21:44:45 +00:00
github-actions[bot]
97c040f946 chore(ui): refresh it control ui locale 2026-05-31 21:44:40 +00:00
github-actions[bot]
f833e96a31 chore(ui): refresh tr control ui locale 2026-05-31 21:44:38 +00:00
github-actions[bot]
9a32c0f85d chore(ui): refresh ar control ui locale 2026-05-31 21:44:35 +00:00
github-actions[bot]
d306f5bf2e chore(ui): refresh fr control ui locale 2026-05-31 21:44:02 +00:00
github-actions[bot]
65d5f7436c chore(ui): refresh ko control ui locale 2026-05-31 21:43:55 +00:00
github-actions[bot]
b78ce079a3 chore(ui): refresh ja-JP control ui locale 2026-05-31 21:43:51 +00:00
github-actions[bot]
6c6cf41b14 chore(ui): refresh es control ui locale 2026-05-31 21:43:42 +00:00
github-actions[bot]
0d79cbab4e chore(ui): refresh pt-BR control ui locale 2026-05-31 21:43:18 +00:00
github-actions[bot]
b04c3e96d6 chore(ui): refresh zh-CN control ui locale 2026-05-31 21:43:11 +00:00
github-actions[bot]
3854a61bea chore(ui): refresh de control ui locale 2026-05-31 21:43:07 +00:00
github-actions[bot]
0d07e30725 chore(ui): refresh zh-TW control ui locale 2026-05-31 21:43:01 +00:00
Ted Li
bfc151e9d3 fix(feishu): preserve long streaming replies
Preserve long Feishu streaming replies by falling oversized finals back to chunked message/static-card delivery instead of closing through an over-limit streaming CardKit payload.

Keeps late-final suppression after a streaming card closes, and uses markdown-aware chunking for static card fallback replies.

Fixes #88631.

Co-authored-by: Ted Li <tl2493@columbia.edu>
2026-05-31 22:41:38 +01:00
Peter Steinberger
b653d94918 chore(lint): enable no-useless-assignment 2026-05-31 22:40:48 +01:00
Andy Ye
49e5091f18 fix(update): recognize manual-update launchd jobs (#88764)
* Recognize manual update launchd jobs

* fix(update): avoid stale launchd false positives

* fix(update): filter stale doctor launchd checks

* fix(update): narrow manual launchd updater labels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:39:37 +01:00
zhang-guiping
cbdb59b255 fix(agents): keep light isolated subagents lightweight
Keep native subagent spawns with `lightContext=true` and resolved isolated context out of context-engine pre-spawn preparation so they remain lightweight.

The normal isolated and forked context-engine lifecycle stays intact, and docs now call out the lightweight isolated exception.

Fixes #81214
2026-05-31 22:37:59 +01:00
Vincent Koc
2ac2a8d210 refactor: share channel operation validation 2026-05-31 23:35:19 +02:00
Shubhankar Tripathy
d042452d20 fix(logging): refresh file log hostname per write
Fix JSONL file-log hostnames getting pinned to `unknown` when the first hostname read returns an empty value. The logger now retries empty hostname reads and caches the first non-empty value, keeping the top-level `hostname` and `_meta.hostname` fields aligned.

Fixes #87258.
Thanks @lonexreb for the fix.

Verification:
- `node scripts/run-vitest.mjs src/logging/logger-redaction-behavior.test.ts src/logger.test.ts`
- `node_modules/.bin/oxfmt --check --threads=1 src/logging/logger.ts src/logging/logger-redaction-behavior.test.ts`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- `gh pr checks 88131 --watch=false`

Co-authored-by: lonexreb <reach2shubhankar@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:35:04 +01:00
Peter Steinberger
50f27ee91d docs: document code-mode MCP API files 2026-05-31 22:33:06 +01:00
charles-openclaw
84266cd30e fix(models): strip remaining provider self prefixes (#88781)
* fix(models): strip remaining provider self prefixes

* fix(models): keep catalog refs prefix-preserving

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:29:47 +01:00
David
61e9961abb fix(agents): expose session status route context
Expose session status route context so agents can distinguish session origin, active live route, and persisted delivery route.

Add maintainer fixup to keep active route metadata on the real live run key when policy and run keys differ.

Thanks @nxmxbbd.

Closes #84544
2026-05-31 22:25:47 +01:00
Ashd.LW.
7c04ce3a79 fix(daemon): preserve container service env across regen
Preserve the current container-related service opt-in environment when regenerating daemon service files, while continuing to drop stale or arbitrary `OPENCLAW_*` variables.

Verification:
- `git diff --check`
- `node scripts/run-vitest.mjs src/commands/daemon-install-helpers.test.ts -t "operator opt-in allowlist"`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt "Review PR #82828 fixup for daemon service env preservation. Focus on whether the allowlist should include only current container opt-in env keys and whether tests cover stale/arbitrary OPENCLAW_* filtering."`
- GitHub CI on `2e4b7f7fccbc46541c9c0ac271b1c97f1a6aa071`

Co-authored-by: wAngByg <281221101+wAngByg@users.noreply.github.com>
2026-05-31 22:22:24 +01:00
Vincent Koc
2ff9e27d4e refactor: share skill proposal workspace handling 2026-05-31 23:21:27 +02:00
Peter Steinberger
5ee3e5d8c0 docs: require real Crabbox visual proof 2026-05-31 22:18:31 +01:00
waterblue
03dec8bb3a fix(openai): avoid replay ids when Responses store is disabled
Avoid replaying prior OpenAI Responses reasoning/message/function-call item ids when the outgoing request disables store, while preserving encrypted reasoning and normalized summary arrays for stateless replay. Keep explicit store-enabled OpenAI wrapper paths opted into item-id replay, and cover shared/simple Responses, ChatGPT/Codex Responses, and GitHub Copilot sanitizer behavior.

Regression tests cover store-disabled id omission, encrypted reasoning preservation, idless Copilot reasoning replay, and direct builder payloads. Local proof included focused Vitest, broad lint, broad test-types, bundled-extension lint, plugin boundary checks, autoreview clean, and live OpenAI Responses gpt-5.5 proof.

Co-authored-by: hang <zhanghang02@gmail.com>
2026-05-31 22:17:32 +01:00
Arnab Saha
5bc80dbe27 fix(diagnostics): carry session UUID on interactive dispatch events
Carry the canonical session UUID from the session store into interactive dispatch diagnostic lifecycle events, matching the cron path so downstream diagnostic consumers can join events back to the JSONL transcript id.

Guard native command redirects by only attaching the UUID when the lifecycle session key matches the session-store lookup key, avoiding a target UUID under a source conversation key.

Verification:
- `pnpm test src/auto-reply/reply/dispatch-from-config.test.ts -t "carries the session store UUID|does not stamp a command target"`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt ...`
- synthetic merge-tree against current `origin/main`

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:15:15 +01:00
Peter Steinberger
8383e2e4d9 fix(doctor): keep post-upgrade JSON stable 2026-05-31 22:12:38 +01:00
Arnab Saha
7f93755206 fix(doctor): post-upgrade entry probe now delegates to install resolver
Address Codex review (P2 x3): replace the duplicate fs.access-based entry
checks in runPostUpgradeProbes with a call to
validatePackageExtensionEntriesForInstall so the doctor probe enforces the
same contract as plugin install/discovery:

- runtimeExtensions shape and length-mismatch validation
- plugin-root boundary enforcement (rejects absolute paths and ../ escapes)
- inferred dist/*.js peer for TypeScript entries; TS source-only entries
  without compiled output are now flagged

Adds 4 regression tests covering the boundary-escape, dist-peer accept,
TS-source-only reject, and runtimeExtensions length-mismatch cases.

Refs: https://github.com/openclaw/openclaw/pull/79260#issuecomment-4403594002
2026-05-31 22:12:38 +01:00
Arnab Saha
7dd1bd894b fix(doctor): drop unused listBuiltRuntimeEntryCandidates import and brace bare if continue 2026-05-31 22:12:38 +01:00
Arnab Saha
6ed6120977 docs(doctor): document --post-upgrade and --json flags 2026-05-31 22:12:38 +01:00
Arnab Saha
0f396368a9 fix(doctor): honor runtimeExtensions before flagging entry_unresolved 2026-05-31 22:12:38 +01:00
Arnab Saha
72679b16eb fix(doctor): resolve plugin index via state-dir helper 2026-05-31 22:12:38 +01:00
Arnab Saha
4a09fd43e2 docs(changelog): note doctor --post-upgrade --json 2026-05-31 22:12:38 +01:00
Arnab Saha
026ab6b882 feat(doctor): expose --post-upgrade and --json CLI flags 2026-05-31 22:12:38 +01:00
Arnab Saha
730492867f feat(doctor): branch into post-upgrade probe runner when --post-upgrade 2026-05-31 22:12:38 +01:00
Arnab Saha
ceda284845 feat(doctor): add plugin.manifest_drift post-upgrade probe 2026-05-31 22:12:38 +01:00
Arnab Saha
8da6b67607 fix(doctor): clean up post-upgrade probe test temp dirs and skip plugins with unreadable package.json 2026-05-31 22:12:38 +01:00
Arnab Saha
e0d3c78042 feat(doctor): add plugin.entry_unresolved post-upgrade probe 2026-05-31 22:12:38 +01:00
Arnab Saha
af7749123b feat(doctor): add post-upgrade finding types 2026-05-31 22:12:38 +01:00
alkor2000
9d97e683d4 feat(doctor): add disk space health check
Add a Doctor health contribution that checks free space on the partition containing the active OpenClaw state directory. Doctor now warns below 500 MB and reports critical below 100 MB so disk pressure is visible before config writes, session transcripts, or log rotation start failing.

The contribution reuses the shared `src/infra/disk-space.ts` probe, runs before state integrity, and is registered in the Doctor health conversion plan with focused coverage for thresholds, formatting, and note behavior.

PR: #59196
Proof: `pnpm test src/commands/doctor-disk-space.test.ts src/flows/doctor-health-conversion-plan.test.ts`; `git diff --check origin/main...HEAD`; `git merge-tree --write-tree origin/main refs/remotes/pr/59196`; GitHub CI run `26720861380`; Real behavior proof run `26720996848`.

Co-authored-by: alkor2000 <200923177@qq.com>
2026-05-31 22:09:36 +01:00
Vincent Koc
e2c745fc58 refactor: share agent wait terminal snapshot 2026-05-31 23:08:28 +02:00
Andy Ye
5df0ed3b9f fix(agents): publish owned announcement session writes
Forward prompt-submission owned session write publication into the embedded session lock controller so same-process announcement/completion writes can advance the requester fence while external edits still trigger takeover protection.

Adds regression coverage for a second controller publishing an owned announcement write and for preserving rejection of a later unowned edit.

Closes #88703.

Thanks @TurboTheTurtle.
2026-05-31 22:00:37 +01:00
Ted Li
e5acae4453 fix(ui): show Workboard comments in edit modal
Show existing Workboard card comments in the edit modal and allow operators to append a new comment through the existing `workboard.cards.comment` gateway method.

Refs #88592.

Verification:
- node scripts/run-vitest.mjs ui/src/ui/views/workboard.test.ts
- pnpm tsgo:test:ui
- git diff --check origin/main...HEAD
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Co-authored-by: Ted Li <tl2493@columbia.edu>
2026-05-31 21:54:26 +01:00
Peter Steinberger
8076eead77 test(channels): narrow pending ingress duplicate 2026-05-31 21:53:42 +01:00
Peter Steinberger
f6365d07c4 fix(agents): wait for cron media completions
Keep cron media generation detached while making cron runs wait for image/music/video completion before final closeout. Records async task IDs, falls back to the task registry for active run-scoped media work, handles timeout races, and scopes no-target generated-media delivery. Fixes #88001.
2026-05-31 21:51:38 +01:00
Sebastien Tardif
9a3e7d4f51 fix(hooks): pass media metadata to internal message_received hook
Forward canonical inbound media metadata to internal message:received hook consumers, matching the plugin received hook mapper and inbound-claim metadata path.

This fixes internal hook handlers losing mediaPath, mediaUrl, mediaType, mediaPaths, mediaUrls, and mediaTypes for received messages with attachments.

Verification:
- node scripts/run-vitest.mjs src/hooks/message-hook-mappers.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Refs: https://github.com/openclaw/openclaw/pull/88740
Thanks @SebTardif.
Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-31 21:49:36 +01:00
Peter Steinberger
ce1165afda fix: repair providerless Codex session overrides
Co-authored-by: Earl Vanze <earlvanze@gmail.com>
2026-05-31 21:45:39 +01:00
brokemac79
90712f6d5e [codex] Surface disabled Codex plugin routes in doctor lint (#88761)
Merged via squash.

Prepared head SHA: 41bcde2d7d
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-31 13:34:53 -07:00
ArthurNie
7c15c2765e fix(feishu): fallback when accepted turns send no visible reply (#87896)
* fix(feishu): fallback when accepted turns send no visible reply

* fix(feishu): cover no-visible-reply fallback gaps

* fix(feishu): mark media replies visible

* fix(feishu): honor suppressed delivery fallback

* test(auto-reply): trim fallback test churn

* fix(feishu): gate empty fallback eligibility

* test(auto-reply): expect fallback metadata after denied dispatch

* fix(feishu): fallback after failed visible final sends

* test(feishu): keep reply dispatcher mock shape aligned

* fix(auto-reply): respect silent policy for no-visible fallback

* fix(feishu): wait for streaming close before fallback

* fix(feishu): clear silent skip before later finals

* fix(feishu): preserve visible state across keepalives

* test(feishu): align lifecycle dispatcher mocks

* fix(feishu): require accepted streaming content for fallback

---------

Co-authored-by: ArthurNie <264332276+ArthurNie@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 21:33:13 +01:00
Peter Steinberger
e681569536 feat: add code-mode MCP API files
* feat: add code-mode MCP API files

* fix: satisfy code-mode MCP lint
2026-05-31 21:29:06 +01:00
Peter Steinberger
b0679d1f13 refactor(channels): store inbound queues in SQLite 2026-05-31 21:15:29 +01:00
Peter Steinberger
80b7f56603 ci: pin Azure crabbox lane to eastus2 2026-05-31 21:11:43 +01:00
Peter Steinberger
995a9bd702 chore(ui): refresh notification i18n metadata 2026-05-31 21:09:37 +01:00
Peter Steinberger
92b9cd21ec test: avoid positional CI check assertion 2026-05-31 16:00:04 -04:00
Peter Steinberger
d62bfab946 ci: split startup and shrinkwrap checks 2026-05-31 15:55:43 -04:00
Peter Steinberger
7aa309319f test(auto-reply): align debounce timer tests 2026-05-31 20:48:02 +01:00
Peter Steinberger
2df95c0b10 chore(lint): enable no-misused-promises 2026-05-31 20:42:13 +01:00
Peter Steinberger
6f58a71582 test(voice-call): install state runtime for events 2026-05-31 20:41:14 +01:00
stain lu
55fc3c10b0 fix(openai/tts): handle speed directives (#74089)
Adds OpenAI speech speed directive parsing with official OpenAI range validation and custom endpoint passthrough. Closes #12163.
2026-05-31 20:35:46 +01:00
Peter Steinberger
b4a6244ef4 ci: split agents core test shard 2026-05-31 15:35:36 -04:00
Peter Steinberger
6b2cb4db67 fix: polish notifications settings UI 2026-05-31 20:35:10 +01:00
Vincent Koc
0715081990 test(agents): narrow bundle mcp e2e setup 2026-05-31 21:31:52 +02:00
WT-WSL
462b52f62c fix(ci): guard workflow template injection
Guard the remaining Windows Testbox workflow ref logging against GitHub Actions template injection by moving `target_ref` through step env before PowerShell reads it.

Extend the local workflow check wrapper to run pinned `zizmor` across every workflow file, and keep Workflow Sanity's CI audit explicit with trusted-base pre-commit and zizmor configs for pull-request runs.

Thanks @WT-WSL for the original report and patch.

Co-authored-by: dev111-actor <captaintobb@outlook.com>
2026-05-31 20:28:40 +01:00
Peter Steinberger
118b9cacf6 refactor: split ACP manager session flows
Split ACP manager session-flow ownership into focused helpers for initialization, status reads, cancellation, and startup identity reconciliation.

Verification:
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.initialize-session.ts src/acp/control-plane/manager.status.ts src/acp/control-plane/manager.cancel-session.ts src/acp/control-plane/manager.startup-identity-reconcile.ts src/acp/control-plane/manager.close-session.ts src/acp/control-plane/manager.turn-runner.ts src/acp/control-plane/manager.runtime-options-commands.ts src/acp/control-plane/manager.types.ts src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `pnpm tsgo:prod`
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.initialize-session.ts src/acp/control-plane/manager.status.ts src/acp/control-plane/manager.cancel-session.ts src/acp/control-plane/manager.startup-identity-reconcile.ts src/acp/control-plane/manager.close-session.ts src/acp/control-plane/manager.turn-runner.ts src/acp/control-plane/manager.runtime-options-commands.ts src/acp/control-plane/manager.types.ts src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `git diff --check`
- `pnpm check:test-types`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub PR checks for #88752 passed

Real behavior proof:
Behavior addressed: ACP manager session-flow ownership is split out of `AcpSessionManager` without changing initialization, status, cancel, startup identity reconciliation, close, turn, or runtime-option behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager/runtime config/runtime handle tests plus prod/test type checks, lint, format check, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` is down to 612 LOC, with init/status/cancel/startup identity flows in focused modules and matching focused tests.
What was not tested: Live ACP backend session initialization/cancel/status against a real external ACP provider.
2026-05-31 20:26:04 +01:00
Peter Steinberger
8cfccca4de docs(changelog): refresh 2026.5.31 notes 2026-05-31 20:24:49 +01:00
Peter Steinberger
01603bbbf4 docs: require WebVNC screenshot verification 2026-05-31 20:21:47 +01:00
Carmen Fernández Ruiz
2e1ae531bd fix: skip disabled skill snapshot env overrides (#79173)
Co-authored-by: hera8939 <279459669+hera8939@users.noreply.github.com>
2026-05-31 20:20:13 +01:00
Peter Steinberger
9c6f7553be test(gateway): widen tailscale hostname mock 2026-05-31 20:13:34 +01:00
Peter Steinberger
ccb50f89da fix(plugins): clarify loader failure guidance 2026-05-31 15:12:22 -04:00
Peter Steinberger
7c5a412b38 fix(whatsapp): satisfy baileys audio peer 2026-05-31 20:10:28 +01:00
Simon Peck
6653193fdb fix(openai): avoid orphan Responses message id replay
Omit provider-owned OpenAI Responses assistant message ids unless the paired reasoning item was replayed immediately before the message. Preserve commentary/final_answer phase metadata so replay quality stays intact without sending orphan msg_* ids that OpenAI rejects.

Verification:
- node scripts/run-vitest.mjs src/agents/openai-transport-stream.test.ts src/agents/openai-responses.reasoning-replay.test.ts src/llm/providers/openai-responses-shared.test.ts
- node scripts/run-oxlint.mjs on touched files
- git diff --check
- live OpenAI Responses API proof with gpt-5.5
- autoreview clean
- PR CI clean on d6902ed1a0

Co-authored-by: latensified <880715+latensified@users.noreply.github.com>
2026-05-31 20:08:33 +01:00
Feelw00
7a3a52cda9 fix(agents): atomic auth.json writes
Persist agent auth files via atomic sibling-temp replacement instead of truncating `auth.json` in place, preventing crash-time credential lockout. Preserve the existing auth directory mode during replacement and keep the credential file at `0600`.

Proof:
- `node scripts/run-vitest.mjs src/agents/sessions/auth-storage.test.ts` passed, 2 tests.
- `git diff --check` passed.
- `autoreview --mode local` clean.
- `autoreview --mode branch --base origin/main` clean.
- GitHub checks green on head `3fb1d767e70118a0e8db5b0fd64d807d456721a8`.

Closes #88028.

Co-authored-by: Feelw00 <dhrtn1006@naver.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:06:50 +01:00
Peter Steinberger
fa2b2ffab4 fix(channels): recover failed progress draft starts (#88749) 2026-05-31 20:06:28 +01:00
charles-openclaw
a6f4de4a66 feat(gateway): support Tailscale Serve service names
Adds optional `gateway.tailscale.serviceName` support for Tailscale Serve so the Gateway Control UI can be exposed through a named Tailscale Service while existing hostname-based Serve and Funnel behavior stays unchanged.

The implementation validates `svc:<dns-label>`, passes the Service name to `tailscale serve`, clears named Service config with `tailscale serve clear <service>` when resetOnExit runs, and uses the derived Service hostname in startup logs, status output, and pairing URLs.

Verification:
- node scripts/run-vitest.mjs src/infra/tailscale.test.ts src/gateway/server-tailscale.test.ts src/config/config.gateway-tailscale-bind.test.ts src/gateway/startup-auth.test.ts src/commands/status.scan.shared.test.ts src/pairing/setup-code.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/infra/tailscale.test.ts src/gateway/server-tailscale.test.ts src/config/config.gateway-tailscale-bind.test.ts src/gateway/startup-auth.test.ts src/commands/status.scan.shared.test.ts src/pairing/setup-code.test.ts"
- git diff --check
- git merge-tree --write-tree origin/main origin/pr/88691

Closes #88629.
Co-authored-by: Charles OpenClaw <charles-openclaw@9bcfae.inboxapi.ai>
2026-05-31 20:05:02 +01:00
Peter Steinberger
b02c448585 docs(plugins): add npm readmes for channel providers 2026-05-31 20:02:45 +01:00
Vladyslav Levchuk
a93240e2c6 fix(ui): show communication notifications tab (#74715)
Expose the existing virtual Communication > Notifications settings tab for Web Push controls, while keeping it out of the unscoped root settings view. Adds browser regression coverage for the scoped virtual tab.

Thanks @VladyslavLevchuk.

Co-authored-by: Vladyslav Levchuk <32742736+VladyslavLevchuk@users.noreply.github.com>
2026-05-31 19:58:55 +01:00
Peter Steinberger
720071a6c6 refactor: extract ACP runtime option commands
Extract ACP runtime-option command flows from `AcpSessionManager` into `manager.runtime-options-commands.ts`.

Verification:
- `pnpm format:fix src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `pnpm tsgo:prod`
- `pnpm test src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts src/acp/control-plane/manager.test.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `git diff --check`
- `pnpm check:test-types`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88747 passed

Real behavior proof:
Behavior addressed: ACP runtime-option mutation ownership moved out of `AcpSessionManager` without changing set-mode, set-config-option, raw update, reset, persistence, or runtime-cache invalidation semantics.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP runtime config/handle/manager tests plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` is down to 885 LOC, with runtime-option command logic isolated in `manager.runtime-options-commands.ts`.
What was not tested: Live ACP backend mode/config option mutation against a real external ACP provider.
2026-05-31 19:56:43 +01:00
Roee Jukin
2155450ed7 fix(acp): prefer clean command text for local bypass
Prefer the clean channel command body when ACP decides whether an inbound message should bypass the agent loop for local OpenClaw commands.

This keeps envelope-wrapped channel text, such as WhatsApp display bodies, from hiding commands like /status when the channel already provided a normalized command body. The ACP runtime prefilter now uses the same command-text resolution as dispatch, and dispatch still requires registry-backed local commands before bypassing.

Co-authored-by: RoeeJ <RoeeJ@users.noreply.github.com>
2026-05-31 19:56:04 +01:00
Jason
e74931778c fix: preserve workspaces during state-only uninstall
Preserve workspace directories when `openclaw uninstall --state` removes local state, including configured workspaces and implicit per-agent workspaces resolved by the runtime. State-only uninstall now uses a cleanup plan that keeps those workspace roots unless `--workspace` is selected.

Fixes #75052.

Proof:
- `git diff --check origin/main...HEAD`
- `pnpm exec oxfmt --check --threads=1 src/commands/cleanup-utils.ts src/commands/cleanup-utils.test.ts src/commands/uninstall.ts src/commands/uninstall.test.ts docs/cli/uninstall.md docs/install/uninstall.md`
- `node scripts/run-vitest.mjs src/commands/uninstall.test.ts src/commands/cleanup-utils.test.ts src/commands/reset.test.ts src/commands/agents.delete.test.ts`
- `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile /tmp/openclaw-pr75061-core-test-final-rebase2.tsbuildinfo`
- `pnpm docs:list`
- `node scripts/check-docs-mdx.mjs docs/cli/uninstall.md docs/install/uninstall.md`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- CI: https://github.com/openclaw/openclaw/actions/runs/26721260691

Co-authored-by: Jason-Bai <boybai.work@gmail.com>
2026-05-31 19:54:34 +01:00
ArthurNie
9d54285b0d fix: force preflight compaction before oversized agent turns
Force required preflight context compaction before oversized turns can enter the agent runtime. Treat required preflight compaction as a hard gate: compact, skip only explicit harmless no-op reasons, or surface a visible recovery message when compaction cannot recover.

Fixes #87234.

Co-authored-by: ArthurNie <264332276+ArthurNie@users.noreply.github.com>
2026-05-31 19:48:49 +01:00
Peter Steinberger
3ff86f3350 refactor: migrate voice-call call logs through doctor (#88731) 2026-05-31 19:43:03 +01:00
Peter Steinberger
2f449285b9 refactor: extract ACP close session flow
Refactor ACP close-session ownership by extracting the runtime close/recovery lifecycle into `manager.close-session.ts`.

Verification:
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts`
- `pnpm tsgo:prod`
- `pnpm check:test-types`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88744 passed

Real behavior proof:
Behavior addressed: ACP close-session ownership moved out of `AcpSessionManager` without changing close/recovery behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager tests covering close-session behavior, runtime config, and runtime handles, plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` dropped from 1149 LOC to 1038 LOC while close-session runtime lifecycle handling lives in `manager.close-session.ts`.
What was not tested: Live ACP backend close/recovery against a real external ACP provider.
2026-05-31 19:42:46 +01:00
Peter Steinberger
465a5456fe fix(agents): preserve disabled subagent delivery state 2026-05-31 19:42:00 +01:00
Federico Kamelhar
ecbd97e968 fix(gateway): rate-limit bootstrap-token verification
Gateway/security: rate-limits pre-auth bootstrap-token verification and serializes per-IP attempts to prevent mutex-stall DoS while preserving device-token fallback.

Fixes #77978.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:40:22 +01:00
Peter Steinberger
ef04c72f08 docs: require live external API tests 2026-05-31 19:39:41 +01:00
Federico Kamelhar
e76df691fe fix(skills): bound watcher workspace state
Bounds skills watcher subscriptions and workspace snapshot-version state to active workspaces on the current `src/skills/runtime` implementation.

The fix keeps shared path watchers as the owner boundary, evicts idle workspace subscriptions after 1 hour without closing watchers still used by other workspaces, and clears per-workspace version keys only after preserving/advancing invalidation so cached skill snapshots cannot miss changes across teardown or re-enable.

Thanks @fede-kamel.

Fixes #77997.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:35:42 +01:00
Vincent Koc
f983111166 perf(scripts): parallelize test group reports 2026-05-31 20:32:54 +02:00
Vincent Koc
7e0d275f7a fix(agents): preserve skipped subagent delivery state 2026-05-31 19:30:32 +01:00
Peter Steinberger
faae7529fd refactor: extract ACP turn runner
Refactor ACP turn execution ownership by extracting the backend attempt and cleanup loop into `manager.turn-runner.ts`.

Verification:
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.turn-results.test.ts src/acp/control-plane/manager.failover.test.ts src/acp/control-plane/manager.runtime-handles.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `pnpm tsgo:prod`
- `pnpm check:test-types`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.turn-runner.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.turn-runner.ts`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88739 passed

Real behavior proof:
Behavior addressed: ACP turn execution ownership moved out of `AcpSessionManager` without changing runtime behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager tests covering turn results, failover, runtime handles, runtime config, plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` dropped from 1495 LOC to 1149 LOC while turn execution lives in `manager.turn-runner.ts`.
What was not tested: Live ACP backend process recovery against a real external ACP provider.
2026-05-31 19:29:47 +01:00
Jeff
01ef169004 fix(agents): sanitize raw HTTP 401 provider errors
Sanitize credential-shaped provider HTTP 401 failures in embedded-agent replies so chat users see a re-authentication hint instead of raw provider text such as `HTTP 401: "Invalid token"`.

The classifier now requires auth classification plus positive 401 evidence, and it stays narrow to credential-shaped failures so billing, scope, replay-invalid, schema, message-only auth, and plain 403 paths keep their existing behavior.

Fixes #56197. Thanks @lokamir.

Co-authored-by: jeffrey701 <jeffreyconradtucker@gmail.com>
2026-05-31 19:26:42 +01:00
zhang-guiping
2fbddce881 fix(cli): avoid catalog validation in agents add (#88314)
Fixes #76284.

Thanks @zhangguiping-xydt.

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
2026-05-31 19:22:16 +01:00
Ben Newell
a88e4fb7e0 fix(memory-core): preserve phase signals on read errors
Phase-signal store reads now recover only missing files and corrupt JSON. Nonrecoverable filesystem read failures propagate so dreaming aborts before overwriting existing phase-signal history with an empty replacement.

Fixes #77881.
Thanks @bennewell35.

Co-authored-by: bennewell35 <newelljben@gmail.com>
2026-05-31 19:18:56 +01:00
Peter Steinberger
90329e2848 refactor: extract ACP runtime resume state
Extract ACP runtime resume/discard recovery helpers from `AcpSessionManager` into `manager.runtime-resume-state.ts`, and share the manager session-meta writer callback type from `manager.types.ts`. Keeps close-time fresh-session recovery, early-turn retry, persisted resume identifier clearing, and discard-persistent-state behavior intact while reducing `manager.core.ts` from 1655 LOC to 1495 LOC.

Proof: focused ACP manager runtime-handle/runtime-config/turn-result tests, prod + test type checks, narrow oxlint, format check, diff check, autoreview clean, PR CI green.
2026-05-31 19:18:18 +01:00
Vincent Koc
454a69a048 test(gateway): align startup refactor expectations 2026-05-31 19:10:25 +01:00
Federico Kamelhar
78f2a89e95 fix(discord): bound REST entity cache growth
Bound DiscordEntityCache entries with a write-time expired-entry sweep and a default 5,000-entry cap while preserving current safe expiry timestamp normalization. This prevents high-cardinality Discord user/channel/guild/member fetches from retaining stale Map entries for the gateway lifetime.

Fixes #77975.
Thanks @fede-kamel.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:08:27 +01:00
Peter Steinberger
3613981579 test(gateway): refresh startup assertions 2026-05-31 19:07:31 +01:00
Sebuh Honarchian
a129b912a4 fix(gateway): guard direct session display names
Guard group display-name generation behind group/channel classification so direct Telegram sessions fall back to their explicit or origin labels. Keep session-list search aligned with that visible fallback.

Fixes #55354.
Thanks @sebuh-infsol.
2026-05-31 19:03:42 +01:00
Peter Steinberger
2a30b937cb refactor: extract ACP runtime handle ensure flow
Extract ACP runtime-handle ensure/reuse/recreate flow into `manager.runtime-handle-ensure.ts`. Keeps `AcpSessionManager` focused on orchestration while preserving backend resolution, resume identity retry, metadata persistence, cache replacement, and concurrency-limit behavior.

Proof: focused ACP manager runtime-handle/runtime-config/turn-result tests, narrow oxlint, prod + test type checks, autoreview clean, PR CI green.
2026-05-31 19:01:59 +01:00
Mert Başar
0ff5fe3a80 fix(auth): add force re-login recovery and fallback auth skips
Summary:
- Add forced provider re-login support that clears cached auth profiles before running provider login again.
- Add provider-auth remediation guidance and a session-scoped skip cache for known-bad fallback auth attempts.
- Wire session ids through agent command, auto-reply, and embedded compaction fallback callers so the skip cache applies on real run paths.
- Fail closed when forced auth profile removal cannot update the profile store.

Verification:
- Local format, lint, diff-check, focused Vitest shards, and autoreview passed.
- PR CI, CodeQL Security High, and Critical Quality agent-runtime-boundary passed on head 1b4e9e753e.

Co-authored-by: Mert Basar <MertBasar0@users.noreply.github.com>
2026-05-31 19:01:51 +01:00
Vincent Koc
db0209ac5d perf(scripts): parallelize remote core oxlint shards 2026-05-31 20:01:41 +02:00
Peter Steinberger
3bac0bcbfb fix(codex): stream final answer partials (#88730) 2026-05-31 19:00:44 +01:00
Youssef Hemimy
beb499b4d1 fix(approvals): interpolate request id in fallback command
Fix approval fallback text so exec and plugin approval messages render a concrete request id in the chat copy-paste command instead of the literal <id> placeholder.

This makes the Reply with: /approve ... line directly usable for owners while keeping the existing approval resolver contract unchanged.

Proof:
- git diff --check origin/main...HEAD
- pnpm test src/infra/exec-approval-forwarder.test.ts src/infra/plugin-approval-forwarder.test.ts src/plugin-sdk/approval-renderers.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- CI run 26720052738 passed

Thanks @itsuzef.
2026-05-31 18:59:43 +01:00
Peter Steinberger
7617d062fd chore(lint): fix rebased lint violations 2026-05-31 18:59:02 +01:00
Peter Steinberger
304e2c83c0 chore(lint): enable stricter oxlint rules 2026-05-31 18:59:02 +01:00
Peter Steinberger
cb569f6ad9 docs: clarify superseded PR close policy 2026-05-31 18:57:32 +01:00
Chunyue Wang
b8f25e9648 fix(memory): serialize qmd writes across processes (#85931)
Serialize QMD update and embed writes with one per-agent store lock so foreground memory search/index and gateway background QMD work do not write the same index.sqlite concurrently.

The embed path now waits for global embed capacity before taking the per-store lock, so queued embeds do not block same-agent foreground updates while no store write is active.

Fixes #66339
Thanks @openperf.

Co-authored-by: Chunyue Wang <16864032@qq.com>
2026-05-31 18:57:15 +01:00
Peter Steinberger
6b0ad98d62 test(extensions): update pairing challenge assertions 2026-05-31 18:56:20 +01:00
Alex Ho
d88767e819 fix(docker): refresh Node base image digests (#84988)
Refresh pinned node:24-bookworm and node:24-bookworm-slim manifest-list digests across the root, smoke, and e2e Dockerfiles. Update digest pin assertions to cover the plugin-binding e2e Dockerfile.

Verified with live Docker digest inspection, targeted Dockerfile tests, root base-runtime build, install-sh smoke build, and plugin-binding e2e build.

Thanks @LibraHo.
2026-05-31 18:55:33 +01:00
Yuval Dinodia
b988e2f92b fix(daemon): detect system-scope systemd gateway units on Linux
Detect OpenClaw gateway units installed in the system systemd scope, including marker-owned custom unit names such as `openclaw.service`. Route status/restart/stop through the system manager when appropriate, and show non-root users the matching `sudo systemctl ...` command instead of falling back to unmanaged process signaling.

Fixes #87577.
Thanks @yetval.

Verification:
- `node scripts/run-vitest.mjs src/daemon/systemd.test.ts src/cli/daemon-cli/lifecycle.test.ts src/daemon/inspect.test.ts src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/status.gather.test.ts src/cli/daemon-cli/response.test.ts src/commands/doctor-gateway-daemon-flow.test.ts src/cli/update-cli/restart-helper.test.ts src/infra/outbound/message-action-runner.core-send.test.ts`
- AWS Crabbox `cbx_69f97dff5e5c`, run `run_a68431b3dad6`: exact SHA checkout, focused tests, real `/etc/systemd/system/openclaw.service` status/restart/stop proof.
2026-05-31 18:52:02 +01:00
Peter Steinberger
e014145ac1 docs: mention markdown host-local media sends (#79658) 2026-05-31 18:51:45 +01:00
Clever
14dbf80c74 Fix explicit text alias extension check 2026-05-31 18:51:45 +01:00
Clever
a9eefeea71 Remove changelog entry from text media PR 2026-05-31 18:51:45 +01:00
Clever
9f7eaf06e1 docs: clarify host-local text media boundary 2026-05-31 18:51:45 +01:00
Clever
7d3fc6f924 docs: update host-local media text policy 2026-05-31 18:51:45 +01:00
Clever
b454677874 Restrict plain text media sends to txt 2026-05-31 18:51:45 +01:00
Clever
d729811224 Add changelog for text document media sends 2026-05-31 18:51:45 +01:00
Clever
1e14f4400f Allow validated text document media sends 2026-05-31 18:51:45 +01:00
Peter Steinberger
d641126c1d feat(plugin-sdk): add typed presentation command actions (#88721)
* feat(plugin-sdk): add typed presentation command actions

* test: use shared env helper in telegram bot tests

* test: expect typed approval actions

* test: expect typed sdk approval actions
2026-05-31 18:48:45 +01:00
Peter Steinberger
4b1d2faa99 docs: harden Codex dependency review gate 2026-05-31 18:48:15 +01:00
Peter Steinberger
058152cf69 refactor: extract ACP manager runtime handle cache
Extract ACP manager runtime-handle cache ownership into a dedicated helper. Keeps the session manager focused on lifecycle orchestration while preserving cached handle reuse, close/clear, idle eviction, matching, and observability behavior.

Proof: focused ACP manager runtime-handle/runtime-config tests, narrow oxlint, pnpm check:test-types, autoreview clean, PR CI green.
2026-05-31 18:46:07 +01:00
Jerry-Xin
56362524ed fix(agents): prefer real tool results over repair synthetics
Ref #84134.

Prefer real tool results over generated missing-result placeholders during transcript repair, including late results after later assistant turns and explicitly marked custom-text repair placeholders. Keep real error outputs such as aborted when they are not generated repair placeholders.

Thanks @Jerry-Xin.

Co-authored-by: 忻役 <xinyi@mininglamp.com>
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
2026-05-31 18:44:37 +01:00
Peter Steinberger
05b3f1c29d docs: require deeper PR review evidence 2026-05-31 18:42:39 +01:00
Sunjae Kim
201bf125af fix(session-store): rewrite generated transcript paths on rollover
Rewrite generated session transcript paths at the shared session-store merge boundary when a persisted session rolls from one session id to another. This prevents patches that carry a stale generated `sessionFile` from leaving a new logical session id attached to the old transcript file, while preserving custom transcript paths.

Refs #65564.

Proof:
- `node scripts/run-vitest.mjs src/config/sessions/sessions.test.ts`
- `node scripts/run-vitest.mjs src/agents/command/session-store.test.ts`
- `git diff --check origin/main...HEAD`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- CI run 26719583889 attempt 2

Co-authored-by: Sunjae Kim <sunjaekim@bigvalue.co.kr>
2026-05-31 18:41:56 +01:00
Peter Steinberger
db40fde88c fix: persist ACP metadata in SQLite (#88724)
* fix: persist acp metadata in sqlite

* test: align session store acp expectations
2026-05-31 18:38:51 +01:00
Peter Steinberger
cdff174ce6 docs: note OpenAI Codex canonical provider 2026-05-31 18:37:30 +01:00
Vincent Koc
1ba9af1693 fix(ui): improve danger callout contrast 2026-05-31 18:36:14 +01:00
1778 changed files with 48140 additions and 15318 deletions

View File

@@ -187,11 +187,13 @@ gh pr view <number> --json additions,deletions,changedFiles \
## Read beyond the diff
- Review the surrounding code path, not just changed lines. Open the caller, callee, data contracts, adjacent tests, and owner module.
- Before any verdict, read enough code to fill this map: changed surface, runtime entry point, owner boundary, one caller, one callee, sibling implementations sharing the invariant, adjacent tests, current `main` behavior, and shipped/dependency/Codex contracts when relevant.
- For large-codebase PRs, sample enough related files to understand the runtime boundary before deciding. Default to more code reading when the change touches agents, gateway, plugins, auth, sessions, process, config, or provider/runtime seams.
- Compare the PR against current `origin/main` behavior. Check whether recent main already changed the same surface.
- Dependency-backed behavior: MUST read upstream docs/source/types before judging API use, defaults, output shapes, errors, timeouts, memory behavior, or compatibility. Do not assume dependency contracts from memory or PR text.
- Judge solution quality, not only correctness. Ask whether the PR is the clean owner-boundary fix or a wart/workaround that should be replaced by a small refactor, moved seam, contract change, or deletion of duplicate logic.
- Mention the main files read when the verdict depends on code-path evidence.
- If the user challenges the verdict or asks whether the idea is really good, resume code reading first. Do not defend, soften, or reverse the verdict until the missing caller/callee/sibling/dependency path is checked.
## Best-fix review loop
@@ -206,6 +208,7 @@ Before verdict:
5. Compare against current `origin/main` and shipped behavior when regression/compat matters.
6. Inspect upstream dependency/Codex source or docs for dependency-backed behavior.
7. Identify at least one alternative fix location or shape, then reject it with evidence.
8. If any required path above is uninspected, keep reading or mark `Remaining uncertainty`; do not call the PR best, blocked, proof-sufficient, or merge-ready.
Review output must include:

View File

@@ -2,7 +2,7 @@
// Secret scanning alert handler for OpenClaw maintainers.
// Usage: node secret-scanning.mjs <command> [options]
import { execFileSync, spawnSync } from "node:child_process";
import { spawnSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
@@ -39,7 +39,9 @@ function gh(args, { json = true, allowFailure = false } = {}) {
stderr: proc.stderr,
};
}
if (!json) return proc.stdout;
if (!json) {
return proc.stdout;
}
try {
return JSON.parse(proc.stdout);
} catch {
@@ -70,7 +72,9 @@ export function loadBodyRedactionResult(locationType, resultFile) {
if (!resultFile) {
fail("Body notifications require a redaction result file from redact-body-if-needed");
}
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
if (!fs.existsSync(resultFile)) {
fail(`File not found: ${resultFile}`);
}
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
if (typeof result.notify_required !== "boolean") {
@@ -182,10 +186,11 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
const discussion = gql?.data?.repository?.discussion;
if (!discussion)
if (!discussion) {
fail(
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
);
}
discussionId = discussion.id;
@@ -205,15 +210,18 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
);
const replies = replyPage?.data?.node?.replies;
if (!replies)
if (!replies) {
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
}
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
hasMoreReplies = replies.pageInfo.hasNextPage;
replyCursor = replies.pageInfo.endCursor;
}
if (reply) return { discussionId, comment: reply };
if (reply) {
return { discussionId, comment: reply };
}
}
hasNextPage = discussion.comments.pageInfo.hasNextPage;
@@ -241,7 +249,9 @@ function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
* Fetch alert metadata + locations. Never exposes .secret.
*/
function cmdFetchAlert(alertNumber) {
if (!alertNumber) fail("Usage: fetch-alert <number>");
if (!alertNumber) {
fail("Usage: fetch-alert <number>");
}
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
@@ -280,17 +290,23 @@ function cmdFetchAlert(alertNumber) {
* Saves full body to a temp file. Prints metadata + file path to stdout.
*/
function cmdFetchContent(locationJson) {
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
if (!locationJson) {
fail("Usage: fetch-content '<location-json>'");
}
const location = JSON.parse(locationJson);
const type = location.type;
const details = location.details;
if (type === "discussion_comment") {
const commentUrl = details.discussion_comment_url;
if (!commentUrl) fail("No discussion_comment_url in location details");
if (!commentUrl) {
fail("No discussion_comment_url in location details");
}
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
if (!urlMatch) {
fail(`Cannot parse discussion comment URL: ${commentUrl}`);
}
const discussionNumber = urlMatch[1];
const discussionCommentDbId = urlMatch[2];
@@ -298,10 +314,11 @@ function cmdFetchContent(locationJson) {
discussionNumber,
discussionCommentDbId,
);
if (!comment)
if (!comment) {
fail(
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
);
}
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
@@ -334,7 +351,9 @@ function cmdFetchContent(locationJson) {
details.issue_comment_url ||
details.pull_request_comment_url ||
details.pull_request_review_comment_url;
if (!commentUrl) fail(`No comment URL in location details`);
if (!commentUrl) {
fail(`No comment URL in location details`);
}
const comment = gh(["api", commentUrl]);
const bodyFile = tmpFile("body.md");
@@ -378,7 +397,9 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "issue_body") {
const issueUrl = details.issue_body_url || details.issue_url;
if (!issueUrl) fail("No issue URL in location details");
if (!issueUrl) {
fail("No issue URL in location details");
}
const issue = gh(["api", issueUrl]);
const bodyFile = tmpFile("body.md");
@@ -414,7 +435,9 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "pull_request_body") {
const prUrl = details.pull_request_body_url || details.pull_request_url;
if (!prUrl) fail("No PR URL in location details");
if (!prUrl) {
fail("No PR URL in location details");
}
const pr = gh(["api", prUrl]);
const bodyFile = tmpFile("body.md");
@@ -490,7 +513,9 @@ function cmdRedactBody(kind, number, bodyFile) {
if (!kind || !number || !bodyFile) {
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
}
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const endpoint =
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
@@ -509,8 +534,12 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
);
}
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
if (!fs.existsSync(currentBodyFile)) {
fail(`File not found: ${currentBodyFile}`);
}
if (!fs.existsSync(redactedBodyFile)) {
fail(`File not found: ${redactedBodyFile}`);
}
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
@@ -541,7 +570,9 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
* Delete a comment (and all its edit history).
*/
function cmdDeleteComment(commentId) {
if (!commentId) fail("Usage: delete-comment <comment-id>");
if (!commentId) {
fail("Usage: delete-comment <comment-id>");
}
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
}
@@ -551,7 +582,9 @@ function cmdDeleteComment(commentId) {
* Delete a discussion comment via GraphQL (and all its edit history).
*/
function cmdDeleteDiscussionComment(nodeId) {
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
if (!nodeId) {
fail("Usage: delete-discussion-comment <node-id>");
}
const result = ghGraphQL(
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
);
@@ -566,9 +599,12 @@ function cmdDeleteDiscussionComment(nodeId) {
* Create a new discussion comment via GraphQL.
*/
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
if (!discussionNodeId || !bodyFile)
if (!discussionNodeId || !bodyFile) {
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const body = fs.readFileSync(bodyFile, "utf8");
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
@@ -586,8 +622,12 @@ function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId)
* Create a new comment from a file.
*/
function cmdRecreateComment(issueNumber, bodyFile) {
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
if (!issueNumber || !bodyFile) {
fail("Usage: recreate-comment <issue-number> <body-file>");
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const result = gh([
"api",
@@ -715,7 +755,9 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
* Close a secret scanning alert.
*/
function cmdResolve(alertNumber, resolution, comment) {
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
if (!alertNumber) {
fail("Usage: resolve <alert-number> [resolution] [comment]");
}
const res = resolution || "revoked";
const resComment = comment || "Content redacted and author notified to rotate credentials.";
@@ -773,8 +815,12 @@ function cmdListOpen() {
* Print a formatted summary table from a JSON results file.
*/
function cmdSummary(jsonFile) {
if (!jsonFile) fail("Usage: summary <json-file>");
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
if (!jsonFile) {
fail("Usage: summary <json-file>");
}
if (!fs.existsSync(jsonFile)) {
fail(`File not found: ${jsonFile}`);
}
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
const lines = [];

View File

@@ -4,11 +4,11 @@ profile: openclaw-check
provider: azure
class: standard
capacity:
market: spot
market: on-demand
strategy: most-available
# Fail closed instead of silently falling back to on-demand while the
# Azure-backed billing account is the default runner path.
fallback: spot-only
# The Azure-backed billing account carries the OpenClaw runner credits; use
# explicit on-demand capacity instead of low-priority spot, whose regional
# quota is too small for broad maintainer proof or parallel Crabbox lanes.
hints: true
actions:
workflow: .github/workflows/crabbox-hydrate.yml
@@ -48,6 +48,10 @@ aws:
# leaking AWS region names into the Azure default capacity fallback list.
region: eu-west-1
rootGB: 400
azure:
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
# same SKUs and can stall provisioning.
location: eastus2
sync:
delete: true
checksum: false
@@ -67,13 +71,16 @@ env:
- OPENCLAW_*
ssh:
user: crabbox
port: "2222"
# Azure coordinator leases expose SSH on 22. The run wrapper can fall back
# from 2222, but `crabbox job run` hydrates via the configured port directly.
port: "22"
jobs:
prewarm:
provider: azure
target: linux
class: standard
market: spot
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
@@ -90,7 +97,8 @@ jobs:
provider: azure
target: linux
class: standard
market: spot
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
@@ -99,7 +107,18 @@ jobs:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
shell: true
command: |
set -euo pipefail
if ! git status --short >/dev/null 2>&1; then
rm -rf .git
git init -q
git add -A
if ! git diff --cached --quiet; then
git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-check-tree
fi
fi
env CI=1 corepack pnpm check --timed
stop: always
testbox-changed:
provider: blacksmith-testbox

View File

@@ -139,3 +139,139 @@ jobs:
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
check-arm:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check-arm"
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
timeout-minutes: 120
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Verify ARM runner
shell: bash
run: |
set -euo pipefail
runner_arch="$(uname -m)"
echo "check-arm runner architecture: ${runner_arch}"
case "$runner_arch" in
aarch64 | arm64)
;;
*)
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
exit 1
;;
esac
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --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 \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -1202,6 +1202,9 @@ jobs:
- check_name: check-guards
task: guards
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-shrinkwrap
task: shrinkwrap
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-prod-types
task: prod-types
runner: blacksmith-4vcpu-ubuntu-2404
@@ -1277,7 +1280,6 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
pnpm deps:shrinkwrap:check
pnpm deps:patches:check
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
@@ -1286,6 +1288,9 @@ jobs:
# build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes.
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:plugin-sdk:strict-smoke
;;
shrinkwrap)
pnpm deps:shrinkwrap:check
;;
prod-types)
pnpm tsgo:prod
;;

View File

@@ -257,7 +257,8 @@ jobs:
return -1;
}
for (let start = input.indexOf("["); start !== -1; start = input.indexOf("[", start + 1)) {
for (const match of input.matchAll(/\[/g)) {
const start = match.index;
const end = arrayEndFrom(start);
if (end === -1) {
continue;

View File

@@ -43,4 +43,4 @@ jobs:
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
telegram_mode: none
secrets: inherit
secrets: inherit # zizmor: ignore[secrets-inherit] Maintainer-dispatched package acceptance lane intentionally forwards its declared live-test secret matrix.

View File

@@ -61,12 +61,14 @@ jobs:
submodules: false
- name: Probe native Windows
env:
TARGET_REF: ${{ inputs.target_ref || github.ref }}
run: |
$ErrorActionPreference = "Stop"
Write-Host "runner=$env:RUNNER_NAME"
Write-Host "machine=$env:COMPUTERNAME"
Write-Host "workspace=$env:GITHUB_WORKSPACE"
Write-Host "target_ref=${{ inputs.target_ref || github.ref }}"
Write-Host "target_ref=$env:TARGET_REF"
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())

View File

@@ -84,6 +84,65 @@ jobs:
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Prepare trusted workflow audit configs
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
fi
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \
> "$trusted_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
if git cat-file -e "${BASE_SHA}:.github/zizmor.yml" 2>/dev/null; then
git show "${BASE_SHA}:.github/zizmor.yml" > "$trusted_zizmor_config"
elif git show "refs/remotes/origin/${BASE_REF}:.github/zizmor.yml" \
> "$trusted_zizmor_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .github/zizmor.yml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted zizmor config unavailable::Could not read .github/zizmor.yml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
python3 - "$trusted_config" "$trusted_zizmor_config" <<'PY'
from pathlib import Path
import sys
config_path = Path(sys.argv[1])
zizmor_config_path = sys.argv[2]
text = config_path.read_text()
if ".github/zizmor.yml" not in text:
raise SystemExit("trusted pre-commit config does not reference .github/zizmor.yml")
config_path.write_text(text.replace(".github/zizmor.yml", zizmor_config_path))
PY
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
- name: Install pre-commit
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
- name: Install actionlint
shell: bash
run: |
@@ -103,6 +162,15 @@ jobs:
- name: Lint workflows
run: actionlint
- name: Audit all workflows with zizmor
shell: bash
run: |
set -euo pipefail
mapfile -t workflow_files < <(
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
)
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
- name: Disallow direct inputs interpolation in composite run blocks
run: python3 scripts/check-composite-action-input-interpolation.py

View File

@@ -22,11 +22,12 @@
"eslint/no-object-constructor": "error",
"eslint/no-param-reassign": "error",
"eslint/no-proto": "error",
"eslint/no-promise-executor-return": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-return-assign": "error",
"eslint/no-sequences": "error",
"eslint/no-self-compare": "error",
"eslint/no-shadow": "off",
"eslint/no-shadow": "error",
"eslint/no-implicit-coercion": "error",
"eslint/no-var": "error",
"eslint/no-useless-call": "error",
@@ -35,7 +36,8 @@
"eslint/no-useless-constructor": "error",
"eslint/no-useless-rename": "error",
"eslint/no-useless-return": "error",
"eslint/no-unused-vars": "off",
"eslint/no-useless-assignment": "error",
"eslint/no-unused-vars": "error",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",
"eslint/no-new-wrappers": "error",
@@ -78,6 +80,7 @@
"typescript/no-extraneous-class": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/no-misused-promises": "error",
"typescript/no-inferrable-types": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/no-unnecessary-qualifier": "error",
@@ -229,15 +232,7 @@
"**/*test-support.ts"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/unbound-method": "off",
"eslint/no-unsafe-optional-chaining": "off"
}
},
{
"files": ["src/agents/embedded-agent-runner/run/attempt.ts"],
"rules": {
"eslint/no-shadow": "error"
"typescript/no-explicit-any": "off"
}
}
]

View File

@@ -10,9 +10,10 @@ Skills own workflows; root owns hard policy and routing.
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
- Reviews/answers: high confidence required. Default to exhaustive relevant codebase search/read, including owners, callers, siblings, tests, docs, and upstream/dependency contracts before verdict. Diff-only review is insufficient.
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work: before any verdict, comment, approval, merge recommendation, or `proof sufficient` claim, inspect sibling `../codex` source for the exact protocol/runtime behavior involved; if missing, clone `https://github.com/openai/codex.git` there first. Do not rely on PR text, OpenClaw wrappers, generated schemas, memory, or prior bot reviews as a substitute. Cite Codex files/lines checked in final/review/comment.
- Review default: read the whole changed function/module plus callers, callees, sibling implementations, adjacent tests, scoped docs, and dependency/Codex contracts before saying `good`, `bad`, `best fix`, `proof sufficient`, or posting a comment. If challenged, keep reading first; do not defend the earlier verdict until the missing path is checked.
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work has a hard gate: the acting agent must personally inspect sibling `../codex` source for the exact protocol/runtime behavior before any verdict, comment, approval, merge recommendation, code change, or `proof sufficient` claim. If missing, clone `https://github.com/openai/codex.git` there first. Subagent reports, PR text, OpenClaw wrappers, generated schemas, memory, and prior bot reviews do not satisfy this gate. No direct `../codex` check means no Codex verdict. Cite Codex files/lines checked in final/review/comment.
- Dependency-backed behavior: read upstream docs/source/types first. No API/default/error/timing guesses.
- External API work: Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
- External API work: live test required. Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
- Live-verify when feasible. Never print secrets.
- Missing deps: `pnpm install`, retry once, then report first actionable error.
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
@@ -30,6 +31,7 @@ Skills own workflows; root owns hard policy and routing.
- 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.
- Every PR review must explicitly ask whether the PR is the best fix, not merely a plausible fix. Verdicts need a best-fix judgment backed by enough code reading to compare owner boundaries, callers, siblings, tests, docs, current `main`, shipped behavior when relevant, and dependency/Codex contracts when involved.
- Before a PR verdict, build a small evidence map: changed surface, entry point, owner boundary, at least one caller and callee, sibling surfaces that share the invariant, existing tests, and current `main` behavior. If any cell is missing, say the gap instead of concluding.
- 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.
@@ -61,6 +63,7 @@ Skills own workflows; root owns hard policy and routing.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- 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`).
- OpenAI Codex is folded into `openai`. No new/live `openai-codex` provider/plugin/auth/model routes; treat them as legacy input only. Runtime/setup/auth/catalog use `openai` + `openai/*`; doctor/migrations repair stale `openai-codex/*` profiles/metadata.
- Config/env surface bar is high; `openclaw.json` and environment variables are already large. Before adding a config option or env var, first prove existing product behavior, provider selection, defaults, or doctor migration cannot solve it. Prefer removing or consolidating config/env options when touching these surfaces. Core supports only the latest config shape; `openclaw doctor --fix` migrates older shipped shapes into the current one.
- CLI setup flows are public API when external docs, installers, or integrations can copy them. Changes to `openclaw onboard`, `openclaw configure`, their documented flags, non-interactive behavior, or generated config shape are compatibility-sensitive API contract changes; prefer additive flags/aliases, deprecation windows, and backward-preserving migrations over breaking existing snippets.
- 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.
@@ -71,6 +74,7 @@ Skills own workflows; root owns hard policy and routing.
- Core runtime consumes only current canonical shapes/config/data. Legacy or retired shapes normalize only in doctor/migration code before runtime; no runtime shims, aliases, or fallback readers.
- State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in `openclaw doctor --fix` migration code only, never steady-state runtime.
- Storage default: SQLite only. Do not add JSON/JSONL/TXT/sidecar files for OpenClaw-owned runtime state, caches, queues, registries, indexes, cursors, checkpoints, or plugin scratch data.
- SQLite runtime access uses Kysely helpers, not raw SQL statement strings, except schema DDL, migrations, low-level DB bootstrap, or narrowly justified SQLite primitives.
- Use the shared state DB (`state/openclaw.sqlite`) for global runtime state and plugin KV data. Use the per-agent DB (`agents/<agentId>/agent/openclaw-agent.sqlite`) for agent-scoped state/cache. Use a dedicated SQLite DB only when schema, volume, or lifecycle clearly does not fit those stores.
- Legacy state/cache files are migration debt. When touching code that reads/writes them, prefer moving the data into SQLite or calling out the refactor follow-up; do not add parallel file paths.
- File storage must be a named product artifact: import/export, user attachment, log, backup, or external tool contract. If it is app state or cache, it belongs in SQLite.
@@ -124,6 +128,7 @@ Skills own workflows; root owns hard policy and routing.
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
@@ -145,6 +150,7 @@ Skills own workflows; root owns hard policy and routing.
- Maintainer decision closes the cluster: if deciding reported behavior/proposed fix is not planned, comment+close all directly associated open issues/PRs unless explicitly told to keep one open. Associated means linked PRs/issues, duplicates, companion workaround PRs, and the canonical issue for the rejected behavior.
- 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.
- Issue/PR work: search strong related issues/PRs before final; close proven dupes/fixed siblings. If none close, suggest one next related follow-up.
- PR superseded by `main`: if code proof shows `main` already has same-or-better behavior, comment canonical commit/PR + focused proof, then close. Bar high: inspect PR diff, current code/tests, linked issue, caller/sibling path. If unsure, leave open.
- Issue/PR numbers need a short summary every time; assume the reader has not opened or read them.
- Before presenting a batch of issues/PRs, use smart subagents to verify live state and current `main`; omit closed/fixed items, and comment+close items already fixed on `main` when maintainer action is authorized.
- 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.
@@ -259,6 +265,7 @@ Skills own workflows; root owns hard policy and routing.
- 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.
- Before sharing WebVNC links, use Crabbox screenshot first; verify real app/path works and target UI is not broken.
- 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`.
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
- `message_tool_only`: normal agent final visible reply = current-source `message(action=send)` only. No `NO_REPLY` prompt/contract; no message call = no source reply. Plugin-owned bound-thread reply = plugin return value; no message tool needed. Never auto-publish private final.

View File

@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.
- Skills, session metadata, gateway runtime state, plugin metadata, and store writes do less repeated work on hot paths while keeping config and dispatch behavior stable.
- Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.
- Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)
- Release, CI, Docker, E2E, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, and status polling so failures report bounded proof instead of stalling.
@@ -35,8 +36,12 @@ Docs: https://docs.openclaw.ai
- Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.
- Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
- Agents/auth: write auth profiles atomically, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state.
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
@@ -52,7 +57,7 @@ Docs: https://docs.openclaw.ai
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.
- Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.
- Agents: accept hidden `sessions_send` body aliases before validation while keeping the model-facing `message` schema canonical. (#88229) Thanks @zhangguiping-xydt.
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.

View File

@@ -9,9 +9,9 @@
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"

View File

@@ -5528,6 +5528,7 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
public let createdat: String
public let updatedat: String
public let createdby: AnyCodable
public let origin: [String: AnyCodable]?
public let proposedversion: String
public let draftfile: String
public let drafthash: String
@@ -5552,6 +5553,7 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
createdat: String,
updatedat: String,
createdby: AnyCodable,
origin: [String: AnyCodable]?,
proposedversion: String,
draftfile: String,
drafthash: String,
@@ -5575,6 +5577,7 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
self.createdat = createdat
self.updatedat = updatedat
self.createdby = createdby
self.origin = origin
self.proposedversion = proposedversion
self.draftfile = draftfile
self.drafthash = drafthash
@@ -5600,6 +5603,7 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
case createdat = "createdAt"
case updatedat = "updatedAt"
case createdby = "createdBy"
case origin
case proposedversion = "proposedVersion"
case draftfile = "draftFile"
case drafthash = "draftHash"

View File

@@ -1,2 +1,2 @@
f1da4b7930475e4be33cb05b8c239f728c7338eb1e8df9b7905bbae94d62da9e plugin-sdk-api-baseline.json
6fd007eede80893680d65c6f245eafb9e6301a1e4306530b0134fd5b3da0cddb plugin-sdk-api-baseline.jsonl
19bdf1196ec771a00777a16fd1e9c3662b8fd788a81034e705c41a74ee79c7ec plugin-sdk-api-baseline.json
43feff80c90adad0f821d1f1e184a9bff1e93d81e6d53a26a26fd9e2972be759 plugin-sdk-api-baseline.jsonl

View File

@@ -248,7 +248,7 @@ iMessage catchup is now available as an opt-in feature on the bundled plugin. On
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set `channels.imessage.enabled: false`, restart the Gateway, fix the `imsg` blocker, and retry the cutover.
The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
The reply cache lives in SQLite plugin state. `openclaw doctor --fix` imports and archives the old `imessage/reply-cache.jsonl` sidecar when present.
## Related

View File

@@ -533,7 +533,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr
</Accordion>
<Accordion title="Message IDs">
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent in-memory reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
</Accordion>
@@ -714,7 +714,7 @@ Each replayed row is fed through the live dispatch path (`evaluateIMessageInboun
### Cursor and retry semantics
Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<account>__<hash>.json` (the OpenClaw state dir defaults to `~/.openclaw`, overridable with `OPENCLAW_STATE_DIR`):
Catchup keeps a per-account cursor in SQLite plugin state:
```json
{
@@ -729,6 +729,7 @@ Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<acco
- After the startup catchup query succeeds, later live-handled rows also advance the same cursor so a gateway restart does not replay messages that were already handled live. Live cursor writes do not jump past catchup failures that are still below `maxFailureRetries`.
- After `maxFailureRetries` consecutive throws against the same `guid`, catchup logs a `warn` and force-advances the cursor past the wedged message so subsequent startups can make progress.
- Already-given-up guids are skipped on sight (no dispatch attempt) on later runs and counted under `skippedGivenUp` in the run summary.
- `openclaw doctor --fix` imports legacy `<openclawStateDir>/imessage/catchup/*.json` cursor files into SQLite plugin state and archives the old files.
### Operator-visible signals

View File

@@ -15,7 +15,7 @@ OpenClaw CI runs on every push to `main` and every pull request. The `preflight`
| Job | Purpose | When it runs |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
| `security-fast` | Private key detection, workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
| `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes |
| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes |
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes |
@@ -80,6 +80,7 @@ apply to that PR.
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed.
- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
- **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph.
- **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed.
- **TUI PTY** is a focused workflow for TUI changes. It runs `node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts` on Linux Node 24 for `src/tui/**`, the watch harness, package script, lockfile, and workflow edits. The required lane uses a deterministic `TuiBackend` fixture; the slower `tui --local` smoke is opt-in with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1` and mocks only the external model endpoint.
- **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly.

View File

@@ -45,6 +45,8 @@ openclaw doctor --deep
openclaw doctor --fix
openclaw doctor --fix --non-interactive
openclaw doctor --generate-gateway-token
openclaw doctor --post-upgrade
openclaw doctor --post-upgrade --json
```
For channel-specific permissions, use the channel probes instead of `doctor`:
@@ -68,7 +70,8 @@ The targeted Discord capabilities probe reports the bot's effective channel perm
- `--allow-exec`: allow doctor to execute configured exec SecretRefs while verifying secrets
- `--deep`: scan system services for extra gateway installs and report recent Gateway supervisor restart handoffs
- `--lint`: run modernized health checks in read-only mode and emit diagnostic findings
- `--json`: with `--lint`, emit JSON findings instead of human output
- `--post-upgrade`: run post-upgrade plugin compatibility probes; emits findings to stdout; exits with code 1 if any error-level findings are present
- `--json`: with `--lint`, emit JSON findings instead of human output; with `--post-upgrade`, emit a machine-readable JSON envelope (`{ probesRun, findings }`)
- `--severity-min <level>`: with `--lint`, drop findings below `info`, `warning`, or `error`
- `--skip <id>`: with `--lint`, skip a check id; repeat to skip more than one
- `--only <id>`: with `--lint`, run only a check id; repeat to run a small selected set
@@ -188,6 +191,16 @@ id is not registered, no check runs for that id; use the command's `checksRun`
and `checksSkipped` fields to verify a focused gate is selecting the checks you
expect.
## Post-upgrade mode
`openclaw doctor --post-upgrade` runs plugin compatibility probes intended to be
chained after a build or upgrade. Findings are emitted to stdout; the command
exits with code 1 if any finding has `level: "error"`. Add `--json` to receive a
machine-readable envelope (`{ probesRun, findings }`) suitable for CI, the
community `fork-upgrade` skill, and other post-upgrade smoke tooling. If the
installed plugin index is missing or malformed, JSON mode still emits that
envelope with a `plugin.index_unavailable` error finding.
Notes:
- In Nix mode (`OPENCLAW_NIX_MODE=1`), read-only doctor checks still work, but `doctor --fix`, `doctor --repair`, `doctor --yes`, and `doctor --generate-gateway-token` are disabled because `openclaw.json` is immutable. Edit the Nix source for this install instead; for nix-openclaw, use the agent-first [Quick Start](https://github.com/openclaw/nix-openclaw#quick-start).

View File

@@ -35,6 +35,7 @@ openclaw uninstall --dry-run
Notes:
- Run `openclaw backup create` first if you want a restorable snapshot before removing state or workspaces.
- `--state` preserves configured workspace directories unless `--workspace` is also selected.
- `--all` is shorthand for removing service, state, workspace, and app together.
- `--non-interactive` requires `--yes`.

View File

@@ -91,7 +91,7 @@ For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by pr
OpenClaw calls two optional subagent lifecycle hooks:
<ParamField path="prepareSubagentSpawn" type="method">
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds.
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds. Native subagent spawns that request `lightContext` and resolve to `contextMode="isolated"` intentionally skip this hook so the child starts from the lightweight bootstrap context without context-engine-managed pre-spawn state.
</ParamField>
<ParamField path="onSubagentEnded" type="method">
Clean up when a subagent session completes or is swept.

View File

@@ -67,6 +67,27 @@ OpenClaw separates the selected provider/model from why it was selected. That so
The auto fallback primary-probe interval is five minutes and is not configurable. OpenClaw remembers recent probes per session and primary model so a failing primary is not retried on every turn. OpenClaw sends a visible notice when a session moves onto fallback and another notice when it returns to the selected primary; it does not repeat the notice on every sticky fallback turn.
## Auth failure skip cache
By default, every new turn keeps the existing fallback retry behavior: OpenClaw
will try each configured fallback candidate again, including non-primary
candidates that recently failed with `auth` or `auth_permanent`.
Operators who prefer to suppress those repeat auth failures can opt in with:
```bash
OPENCLAW_FALLBACK_SKIP_TTL_MS=60000
```
When enabled, OpenClaw records an in-memory, session-scoped skip marker for a
non-primary fallback candidate after an auth-class failure. The marker is keyed
by session id, provider, and model. Primary candidates are never skipped, so an
explicit user model selection still surfaces the real auth error. The cache is
process-local and clears on Gateway restart.
The value is a TTL in milliseconds. `0` or an unset value disables the cache.
Positive values are clamped between 1 second and 10 minutes.
## User-visible fallback notices
When a session moves onto an auto-selected fallback, OpenClaw sends a status notice in the same reply surface:

View File

@@ -120,6 +120,19 @@ sparse token/cache counters from the latest transcript usage entry, and
the caller's current session; visible client labels such as `openclaw-tui` are
not session keys.
When route metadata is available, `session_status` also includes a visible
`Route context` JSON block and matching structured `details` fields. These
fields disambiguate the session key from the route that is currently handling
the live run:
- `origin` is where the session was created, or the provider inferred from a
deliverable session-key prefix when older state lacks stored origin metadata.
- `active` is the current live-run route. It is only reported for the live or
current session being handled now.
- `deliveryContext` is the persisted delivery route stored on the session,
which OpenClaw can reuse for later delivery even when the active surface
differs.
`sessions_yield` intentionally ends the current turn so the next message can be
the follow-up event you are waiting for. Use it after spawning sub-agents when
you want completion results to arrive as the next message instead of building

View File

@@ -206,6 +206,17 @@ openclaw models auth login --provider openai --profile-id openai:lain
This is the easiest way to keep multiple OAuth logins for the same provider
separate inside one agent.
Use `--force` when a saved provider profile is stuck, expired, or tied to the
wrong account and the normal login command keeps reusing it. `--force` deletes
the saved auth profiles for that provider in the selected agent directory, then
runs the same provider auth flow again. It does not revoke credentials at the
provider; rotate or revoke them in the provider dashboard when you need
provider-side invalidation.
```bash
openclaw models auth login --provider anthropic --force
```
### Per-session (chat command)
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`).

View File

@@ -76,6 +76,37 @@ Server globs use the provider-safe MCP server prefix, not necessarily the raw `m
Without that sandbox-layer entry, the MCP server can still load successfully while its tools are filtered before the provider request. Use `openclaw doctor` to catch this shape for OpenClaw-managed servers in `mcp.servers`. MCP servers loaded from bundled plugin manifests or Claude `.mcp.json` use the same sandbox gate, but this diagnostic does not enumerate those sources yet; use the same allowlist entries if their tools disappear in sandboxed turns.
### `tools.codeMode`
`tools.codeMode` enables the generic OpenClaw code-mode surface. When enabled
for a run with tools, the model sees only `exec` and `wait`; normal OpenClaw
tools move behind the in-sandbox `tools.*` catalog bridge, and MCP tools are
available through the generated `MCP` namespace.
```json5
{
tools: {
codeMode: {
enabled: true,
},
},
}
```
The shorthand is also accepted:
```json5
{
tools: { codeMode: true },
}
```
MCP declarations are exposed through the read-only virtual API file surface in
code mode. Guest code can call `API.list("mcp")` and
`API.read("mcp/<server>.d.ts")` to inspect TypeScript-style signatures before
calling `MCP.<server>.<tool>()`. See [Code mode](/reference/code-mode) for the
runtime contract, limits, and debugging steps.
### `tools.allow` / `tools.deny`
Global tool allow/deny policy (deny wins). Case-insensitive, supports `*` wildcards. Applied even when Docker sandbox is off.

View File

@@ -580,6 +580,11 @@ See [Inferred commitments](/concepts/commitments).
value, so repeated failures from one localhost origin do not automatically
lock out a different origin.
- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
- `tailscale.serviceName`: optional Tailscale Service name for Serve mode, such
as `svc:openclaw`. When set, OpenClaw passes it to `tailscale serve
--service` so the Control UI can be exposed through a named Service instead
of the device hostname. The value must use Tailscale's `svc:<dns-label>`
Service name format; startup reports the derived Service URL.
- `tailscale.preserveFunnel`: when `true` and `tailscale.mode = "serve"`, OpenClaw
checks `tailscale funnel status` before re-applying Serve at startup and skips
it if an externally configured Funnel route already covers the gateway port.

View File

@@ -67,6 +67,25 @@ and use `gateway.auth.mode: "token"` or `"password"`.
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
To expose the Control UI through a named Tailscale Service instead of the
device hostname, set `gateway.tailscale.serviceName` to the Service name:
```json5
{
gateway: {
bind: "loopback",
tailscale: { mode: "serve", serviceName: "svc:openclaw" },
},
}
```
With the example above, startup reports the Service URL as
`https://openclaw.<tailnet-name>.ts.net/` instead of the device hostname.
Tailscale Services require the host to be an approved tagged node in your
tailnet. Configure the tag and approve the Service in Tailscale before enabling
this option, otherwise `tailscale serve --service=...` will fail during gateway
startup.
### Tailnet-only (bind to Tailnet IP)
Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
@@ -114,6 +133,11 @@ openclaw gateway --tailscale funnel --auth password
- Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in.
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
- `gateway.tailscale.serviceName` applies only to Serve mode and is passed to
`tailscale serve --service=<name>`. The value must use Tailscale's
`svc:<dns-label>` Service name format, for example `svc:openclaw`.
Tailscale requires Service hosts to be tagged nodes, and the Service may need
approval in the admin console before Serve can publish it.
- Set `gateway.tailscale.resetOnExit` if you want OpenClaw to undo `tailscale serve`
or `tailscale funnel` configuration on shutdown.
- Set `gateway.tailscale.preserveFunnel: true` to keep an externally configured

View File

@@ -1775,7 +1775,7 @@ lives on the [Models FAQ](/help/faq-models).
- The target channel supports outbound media and isn't blocked by allowlists.
- The file is within the provider's size limits (images are resized to max 2048px).
- `tools.fs.workspaceOnly=true` keeps local-path sends limited to workspace, temp/media-store, and sandbox-validated files.
- `tools.fs.workspaceOnly=false` lets structured local media sends use host-local files the agent can already read, but only for media plus safe document types (images, audio, video, PDF, and Office docs). Plain text and secret-like files are still blocked.
- `tools.fs.workspaceOnly=false` lets structured local media sends use host-local files the agent can already read, but only for media plus safe document types (images, audio, video, PDF, Office docs, and validated text documents such as Markdown/MD, TXT, JSON, YAML, and YML). This is not a secret scanner: an agent-readable `secret.txt` or `config.json` can be attached when the extension and content validation match. Keep sensitive files outside agent-readable paths, or keep `tools.fs.workspaceOnly=true` for stricter local-path sends.
See [Images](/nodes/images).

View File

@@ -19,6 +19,8 @@ Recommended: use the built-in uninstaller:
openclaw uninstall
```
When using the CLI, state removal preserves configured workspace directories unless you also select `--workspace`.
Non-interactive (automation / npx):
```bash
@@ -47,6 +49,7 @@ rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
```
If you set `OPENCLAW_CONFIG_PATH` to a custom location outside the state dir, delete that file too.
If you want to keep a workspace inside the state dir, such as `~/.openclaw/workspace`, move it aside before running `rm -rf` or delete state contents selectively.
4. Delete your workspace (optional, removes agent files):

View File

@@ -52,8 +52,14 @@ type MessagePresentationBlock =
| { type: "buttons"; buttons: MessagePresentationButton[] }
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
type MessagePresentationAction =
| { type: "command"; command: string }
| { type: "callback"; value: string };
type MessagePresentationButton = {
label: string;
action?: MessagePresentationAction;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
url?: string;
webApp?: { url: string };
@@ -67,7 +73,9 @@ type MessagePresentationButton = {
type MessagePresentationOption = {
label: string;
value: string;
action?: MessagePresentationAction;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
};
type ReplyPayloadDelivery = {
@@ -83,8 +91,13 @@ type ReplyPayloadDelivery = {
Button semantics:
- `value` is an application action value routed back through the channel's
existing interaction path when the channel supports clickable controls.
- `action.type: "command"` runs a native slash command through core's command
path. Use this for built-in command buttons and menus.
- `action.type: "callback"` carries opaque plugin data through the channel's
interaction path. Channel plugins must not reinterpret callback data as slash
commands.
- `value` is the legacy opaque callback value. New controls should use `action`
so channel plugins can map commands and callbacks without guessing from text.
- `url` is a link button. It can exist without `value`.
- `webApp` describes a channel-native web app button. Telegram renders this
as `web_app` and only supports it in private chats. `web_app` is still
@@ -106,7 +119,8 @@ Button semantics:
Select semantics:
- `options[].value` is the selected application value.
- `options[].action` has the same command/callback meaning as button `action`.
- `options[].value` is the legacy selected application value.
- `placeholder` is advisory and may be ignored by channels without native
select support.
- If a channel does not support selects, fallback text lists the labels.

View File

@@ -311,7 +311,8 @@ The branch already has a real shared SQLite base:
`delivery_queue_entries`, `model_capability_cache`,
`workspace_setup_state`, `native_hook_relay_bridges`,
`current_conversation_bindings`, `plugin_binding_approvals`,
`tui_last_sessions`, `task_runs`, `task_delivery_state`, `flow_runs`,
`tui_last_sessions`, `acp_sessions`, `acp_replay_sessions`,
`acp_replay_events`, `task_runs`, `task_delivery_state`, `flow_runs`,
`subagent_runs`, `migration_runs`, and `backup_runs`.
- Arbitrary plugin-owned state does not get host-owned typed tables. Installed
plugins use `plugin_state_entries` for versioned JSON payloads and
@@ -1669,6 +1670,8 @@ Move these into agent databases:
- ACP replay ledger sessions. Done for runtime writes via
`acp_replay_sessions` and `acp_replay_events`; legacy `acp/event-ledger.json`
remains only as doctor input.
- ACP session metadata. Done for runtime writes via `acp_sessions`; legacy
`entry.acp` blocks in `sessions.json` are doctor migration input only.
- Trajectory sidecars when they are not explicit export files. Done for runtime
writes: trajectory capture writes agent-database `trajectory_runtime_events`
rows and mirrors run-scoped artifacts into SQLite. Legacy sidecars are doctor

View File

@@ -100,6 +100,11 @@ The shorthand is also accepted:
Code mode remains off when `tools.codeMode` is omitted, `false`, or an object
without `enabled: true`.
When you use sandboxed agents with configured MCP servers, also make sure the
sandbox tool policy allows the bundled MCP plugin, for example with
`tools.sandbox.tools.alsoAllow: ["bundle-mcp"]`. See
[Configuration - tools and custom providers](/gateway/config-tools#mcp-and-plugin-tools-inside-sandbox-tool-policy).
Use explicit limits when you want tighter bounds:
```json5
@@ -441,12 +446,13 @@ const hits = await tools.web_search({ query: "OpenClaw code mode" });
MCP catalog entries are not callable through `tools.call(...)` or convenience
functions in code mode. They are exposed only through the generated `MCP`
namespace, which includes TypeScript-style API headers for discovery:
namespace. TypeScript-style declaration files are available through the
read-only `API` virtual file surface, so agents can inspect MCP signatures
without adding MCP schemas to the prompt:
```typescript
const servers = await MCP.$api();
const githubApi = await MCP.github.$api();
const createIssueApi = await MCP.github.$api("createIssue", { schema: true });
const files = await API.list("mcp");
const githubApi = await API.read("mcp/github.d.ts");
const issue = await MCP.github.createIssue({
owner: "openclaw",
@@ -462,7 +468,8 @@ const prompt = await MCP.docs.prompts.get({
});
```
`MCP.<server>.$api()` returns a compact header inferred from MCP tool metadata:
`API.read("mcp/<server>.d.ts")` returns compact declarations inferred from MCP
tool metadata:
```typescript
type McpToolResult = {
@@ -491,6 +498,20 @@ declare namespace MCP.github {
}
```
The declaration files are virtual, not files written under the workspace or
state directory. For each code-mode `exec` call, OpenClaw builds the run-scoped
tool catalog, keeps the visible MCP entries, renders `mcp/index.d.ts` plus one
`mcp/<server>.d.ts` declaration per visible server, and injects that small
read-only table into the QuickJS worker. Guest code sees only the `API` object:
`API.list(prefix?)` returns file metadata and `API.read(path)` returns the
selected declaration content. Unknown paths and `.` / `..` segments are rejected.
This keeps large MCP schemas out of the model prompt. The agent learns that the
virtual API exists from the `exec` tool description, reads only the needed
declaration file, and then calls `MCP.<server>.<tool>()` with one object argument.
`MCP.<server>.$api()` remains available as an inline fallback when the agent
needs a single-tool schema response inside the program.
The guest runtime must not expose host objects directly. Inputs and outputs cross
the bridge as JSON-compatible values with explicit size caps.
@@ -981,8 +1002,9 @@ Code mode coverage should prove:
- all effective non-MCP tools appear in `ALL_TOOLS`
- denied tools do not appear in `ALL_TOOLS`
- `tools.search`, `tools.describe`, and `tools.call` work for OpenClaw tools
- MCP namespace `$api()` returns TypeScript-style headers inferred from MCP
schemas
- `API.list("mcp")` and `API.read("mcp/<server>.d.ts")` expose TypeScript-style
MCP declarations without a bridge/tool call
- MCP namespace `$api()` remains available as an inline fallback for schemas
- MCP namespace calls work for visible MCP tools with one object input, while
direct MCP catalog entries are absent from `tools.*`
- Tool Search control tools are hidden from both the model surface and the hidden
@@ -1014,8 +1036,8 @@ Run these as integration or end-to-end tests when changing the runtime:
7. In `exec`, read `ALL_TOOLS` and assert the effective test tools are present.
8. In `exec`, call OpenClaw/plugin/client tools through `tools.search`,
`tools.describe`, and `tools.call`.
9. In `exec`, call `MCP.$api()` and `MCP.<server>.$api()` and assert the headers
describe visible MCP tools.
9. In `exec`, call `API.list("mcp")` and `API.read("mcp/<server>.d.ts")` and
assert the declaration files describe visible MCP tools.
10. In `exec`, call MCP tools through `MCP.<server>.<tool>({ ...input })` and
assert direct MCP catalog entries are absent from `ALL_TOOLS` and `tools.*`.
11. Assert denied tools are absent and cannot be called by guessed id.

View File

@@ -212,9 +212,9 @@ Local-path behavior follows the same file-read trust model as the agent:
- If `tools.fs.workspaceOnly` is `true`, outbound local media paths stay restricted to the OpenClaw temp root, the media cache, agent workspace paths, and sandbox-generated files.
- If `tools.fs.workspaceOnly` is `false`, outbound local media can use host-local files the agent is already allowed to read.
- Local paths can be absolute, workspace-relative, or home-relative with `~/`.
- Host-local sends still only allow media and safe document types (images, audio, video, PDF, and Office documents). Plain text and secret-like files are not treated as sendable media.
- Host-local sends still only allow media and safe document types (images, audio, video, PDF, Office documents, and validated text documents such as Markdown/MD, TXT, JSON, YAML, and YML). This is an extension of the existing host-read trust boundary, not a secret scanner: if the agent can read a host-local `secret.txt` or `config.json`, it can attach that file when the extension and content validation match.
That means generated images/files outside the workspace can now send when your fs policy already allows those reads, without reopening arbitrary host-text attachment exfiltration.
That means generated images/files outside the workspace can now send when your fs policy already allows those reads, while arbitrary host-local text extensions remain blocked. Keep sensitive files outside the agent-readable filesystem, or keep `tools.fs.workspaceOnly=true` for stricter local-path sends.
## Operations checklist

View File

@@ -37,8 +37,8 @@ async function startRealService(state: DeferredServiceState): Promise<AcpRuntime
throw new Error("ACPX runtime service is not started");
}
state.startPromise ??= (async () => {
const { createAcpxRuntimeService } = await loadServiceModule();
const service = createAcpxRuntimeService(state.params);
const { createAcpxRuntimeService: createAcpxRuntimeServiceLocal } = await loadServiceModule();
const service = createAcpxRuntimeServiceLocal(state.params);
state.realService = service;
await service.start(state.ctx as OpenClawPluginServiceContext);
const backend = getAcpRuntimeBackend(ACPX_BACKEND_ID);

View File

@@ -262,7 +262,12 @@ async function terminatePids(
deps: AcpxProcessCleanupDeps | undefined,
): Promise<number[]> {
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
const sleep = deps?.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
const sleep =
deps?.sleep ??
((ms) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
}));
const terminated: number[] = [];
for (const pid of pids) {
@@ -302,7 +307,7 @@ export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
return { inspectedPids: [], terminatedPids: [], skippedReason: "missing-root" };
}
let processes: AcpxProcessInfo[] = [];
let processes: AcpxProcessInfo[];
try {
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
} catch {

View File

@@ -335,7 +335,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
});
await expect(async () => {
for await (const eventValue of runtime.runTurn({
for await (const ignoredEventValue of runtime.runTurn({
handle: {
sessionKey: "agent:codex:acp:test",
backend: "acpx",
@@ -346,6 +346,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
mode: "prompt",
requestId: "turn-1",
})) {
void ignoredEventValue;
// no-op
}
}).rejects.toMatchObject({
@@ -568,7 +569,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
}),
);
for await (const eventValue of runtime.runTurn({
for await (const ignoredEventValue of runtime.runTurn({
handle: {
sessionKey: "agent:codex:acp:test",
backend: "acpx",
@@ -579,6 +580,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
mode: "prompt",
requestId: "turn-1",
})) {
void ignoredEventValue;
// no-op
}
@@ -599,7 +601,8 @@ describe("AcpxRuntime fresh reset wrapper", () => {
mode: "prompt",
requestId: "turn-2",
});
for await (const eventValue of turn.events) {
for await (const ignoredEventValue of turn.events) {
void ignoredEventValue;
// no-op
}
await turn.result;
@@ -947,16 +950,16 @@ describe("AcpxRuntime fresh reset wrapper", () => {
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
acpxRecordId: "stale",
});
expect(baseStore.load).toHaveBeenCalledTimes(1);
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
await runtime.prepareFreshSession({
sessionKey: "agent:codex:acp:binding:test",
});
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
expect(baseStore.load).toHaveBeenCalledTimes(1);
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
expect(baseStore.load).toHaveBeenCalledTimes(1);
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
await wrappedStore.save({
acpxRecordId: "fresh-record",
@@ -966,7 +969,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
acpxRecordId: "stale",
});
expect(baseStore.load).toHaveBeenCalledTimes(2);
expect(baseStore["load"]).toHaveBeenCalledTimes(2);
});
it("marks the session fresh after discardPersistentState close", async () => {
@@ -998,7 +1001,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
discardPersistentState: true,
});
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
expect(baseStore.load).toHaveBeenCalledOnce();
expect(baseStore["load"]).toHaveBeenCalledOnce();
});
it("cleans up OpenClaw-owned ACPX process trees after close", async () => {

View File

@@ -1196,7 +1196,7 @@ export class AcpxRuntime implements AcpRuntime {
const record = await this.sessionStore.load(
input.handle.acpxRecordId ?? input.handle.sessionKey,
);
let closeSucceeded = false;
let closeSucceeded;
try {
await this.resolveDelegateForLoadedRecord(input.handle, record).close({
handle: input.handle,

View File

@@ -2958,7 +2958,9 @@ describe("active-memory plugin", () => {
};
plugin.register(api as unknown as OpenClawPluginApi);
runEmbeddedAgent.mockImplementationOnce(async (params: { timeoutMs?: number }) => {
await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 5));
await new Promise((resolve) => {
setTimeout(resolve, (params.timeoutMs ?? 0) + 5);
});
return {
payloads: [{ text: "late timeout payload that should never become memory context" }],
meta: { aborted: true },
@@ -3001,7 +3003,9 @@ describe("active-memory plugin", () => {
};
plugin.register(api as unknown as OpenClawPluginApi);
runEmbeddedAgent.mockImplementationOnce(async () => {
await new Promise((resolve) => setTimeout(resolve, CONFIGURED_TIMEOUT_MS + 5));
await new Promise((resolve) => {
setTimeout(resolve, CONFIGURED_TIMEOUT_MS + 5);
});
return { payloads: [{ text: "remember the ramen place" }] };
});
@@ -3131,7 +3135,9 @@ describe("active-memory plugin", () => {
},
},
]);
await new Promise((resolve) => setTimeout(resolve, 35));
await new Promise((resolve) => {
setTimeout(resolve, 35);
});
return { payloads: [{ text: "User usually orders ramen." }] };
});
@@ -3221,7 +3227,9 @@ describe("active-memory plugin", () => {
},
},
]);
await new Promise((resolve) => setTimeout(resolve, 35));
await new Promise((resolve) => {
setTimeout(resolve, 35);
});
return { payloads: [{ text: "User usually orders ramen after late flights." }] };
});

View File

@@ -605,25 +605,25 @@ function resolveRecallRunChannelContext(params: {
(!explicitProvider || explicitProvider === "webchat")
? runnableExplicitChannel
: undefined;
const resolveReturnValue = (params: {
const resolveReturnValue = (paramsLocal: {
resolvedChannel?: string;
resolvedChannelStrength?: "strong" | "weak";
}) => {
const trustedResolvedChannel =
params.resolvedChannelStrength === "strong" ? params.resolvedChannel : undefined;
paramsLocal.resolvedChannelStrength === "strong" ? paramsLocal.resolvedChannel : undefined;
return {
messageChannel:
trustedExplicitChannel ??
trustedResolvedChannel ??
explicitProvider ??
runnableExplicitChannel ??
params.resolvedChannel,
paramsLocal.resolvedChannel,
messageProvider:
trustedExplicitChannel ??
trustedResolvedChannel ??
explicitProvider ??
runnableExplicitChannel ??
params.resolvedChannel,
paramsLocal.resolvedChannel,
};
};
const resolvedSessionKey =
@@ -1793,7 +1793,9 @@ function watchTerminalMemorySearchResult(params: {
if (stopped) {
return;
}
timeoutId = setTimeout(tick, TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS);
timeoutId = setTimeout(() => {
void tick();
}, TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS);
timeoutId.unref?.();
};
const tick = async () => {

View File

@@ -0,0 +1,11 @@
# OpenClaw Amazon Bedrock Mantle Provider
Official OpenClaw provider plugin for routing Amazon Bedrock Mantle models through OpenAI-compatible provider flows.
Install from OpenClaw:
```bash
openclaw plugin add @openclaw/amazon-bedrock-mantle-provider
```
Use this plugin when your Bedrock deployment exposes Mantle-compatible model routing and you want OpenClaw agents to address those models through the Bedrock Mantle provider.

View File

@@ -1,4 +1,4 @@
import type { Api, Model } from "openclaw/plugin-sdk/llm";
import type { Model } from "openclaw/plugin-sdk/llm";
import { describe, expect, it, vi } from "vitest";
import {
createMantleAnthropicStreamFn,

View File

@@ -0,0 +1,11 @@
# OpenClaw Amazon Bedrock Provider
Official OpenClaw provider plugin for Amazon Bedrock. It adds Bedrock model discovery, text generation, embeddings, and guardrail-aware provider routing for agents that use AWS-hosted models.
Install from OpenClaw:
```bash
openclaw plugin add @openclaw/amazon-bedrock-provider
```
Configure AWS credentials and region through your normal OpenClaw credential/profile setup, then select Bedrock models with the `amazon-bedrock/...` provider prefix.

View File

@@ -365,29 +365,29 @@ export async function createBedrockEmbeddingProvider(
const embedQuery = async (
text: string,
options?: { signal?: AbortSignal },
optionsValue?: { signal?: AbortSignal },
): Promise<number[]> => {
if (!text.trim()) {
return [];
}
if (isCohere) {
return (await embedCohere([text], "search_query", options?.signal))[0] ?? [];
return (await embedCohere([text], "search_query", optionsValue?.signal))[0] ?? [];
}
return embedSingle(text, options?.signal);
return embedSingle(text, optionsValue?.signal);
};
const embedBatch = async (
texts: string[],
options?: { signal?: AbortSignal },
optionsLocal?: { signal?: AbortSignal },
): Promise<number[][]> => {
if (texts.length === 0) {
return [];
}
if (isCohere) {
return embedCohere(texts, "search_document", options?.signal);
return embedCohere(texts, "search_document", optionsLocal?.signal);
}
return Promise.all(
texts.map((t) => (t.trim() ? embedSingle(t, options?.signal) : Promise.resolve([]))),
texts.map((t) => (t.trim() ? embedSingle(t, optionsLocal?.signal) : Promise.resolve([]))),
);
};

View File

@@ -272,7 +272,7 @@ describe("amazon-bedrock provider plugin", () => {
setBedrockAppProfileControlPlaneForTest((region) => ({
async getInferenceProfile(input) {
class GetInferenceProfileCommand {
constructor(readonly input: Record<string, unknown> = {}) {}
constructor(readonly inputLocal: Record<string, unknown> = {}) {}
}
bedrockClientConfigs.push(region ? { region } : {});
return await sendBedrockCommand(new GetInferenceProfileCommand(input));

View File

@@ -0,0 +1,11 @@
# OpenClaw Anthropic Vertex Provider
Official OpenClaw provider plugin for Claude models hosted through Google Vertex AI.
Install from OpenClaw:
```bash
openclaw plugin add @openclaw/anthropic-vertex-provider
```
Configure Google Cloud credentials and the target Vertex project/region in OpenClaw, then select Claude models with the Anthropic Vertex provider.

View File

@@ -127,11 +127,11 @@ export default definePluginEntry({
: undefined;
},
normalizeResolvedModel: ({ model }) => normalizeArceeResolvedModel(model),
normalizeTransport: ({ api, baseUrl }) => {
normalizeTransport: ({ api: apiLocal, baseUrl }) => {
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(baseUrl);
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl
? {
api,
api: apiLocal,
baseUrl: normalizedBaseUrl,
}
: undefined;

View File

@@ -495,7 +495,10 @@ export async function startGatewayBonjourAdvertiser(
return { responder, services };
}
async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) {
async function stopCycle(
cycle: BonjourCycle | null,
optsValue?: { shutdownResponder?: boolean },
) {
if (!cycle) {
return;
}
@@ -507,7 +510,7 @@ export async function startGatewayBonjourAdvertiser(
}
}
try {
if (opts?.shutdownResponder) {
if (optsValue?.shutdownResponder) {
await cycle.responder.shutdown();
}
} catch {
@@ -615,7 +618,7 @@ export async function startGatewayBonjourAdvertiser(
}
};
const recreateAdvertiser = async (reason: string, opts?: { stuckState?: boolean }) => {
const recreateAdvertiser = async (reason: string, optsLocal?: { stuckState?: boolean }) => {
if (stopped || disabled) {
return;
}
@@ -624,7 +627,9 @@ export async function startGatewayBonjourAdvertiser(
}
recreatePromise = (async () => {
consecutiveRestarts += 1;
consecutiveStuckStateRestarts = opts?.stuckState ? consecutiveStuckStateRestarts + 1 : 0;
consecutiveStuckStateRestarts = optsLocal?.stuckState
? consecutiveStuckStateRestarts + 1
: 0;
const now = Date.now();
while (
restartTimestamps.length > 0 &&

View File

@@ -530,7 +530,7 @@ export function createBrowserTool(opts?: {
});
const proxyRequest = nodeTarget
? async (opts: {
? async (optsLocal: {
method: string;
path: string;
query?: Record<string, string | number | boolean | undefined>;
@@ -540,12 +540,12 @@ export function createBrowserTool(opts?: {
}) => {
const proxy = await callBrowserProxy({
nodeId: nodeTarget.nodeId,
method: opts.method,
path: opts.path,
query: opts.query,
body: opts.body,
timeoutMs: opts.timeoutMs,
profile: opts.profile,
method: optsLocal.method,
path: optsLocal.path,
query: optsLocal.query,
body: optsLocal.body,
timeoutMs: optsLocal.timeoutMs,
profile: optsLocal.profile,
});
const mapping = await persistProxyFiles(proxy.files);
applyProxyPaths(proxy.result, mapping);

View File

@@ -232,12 +232,13 @@ describe("cdp-proxy-bypass", () => {
describe("withNoProxyForLocalhost concurrency", () => {
it("does not leak NO_PROXY when called concurrently", async () => {
await withIsolatedNoProxyEnv(async () => {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostScoped } =
await import("./cdp-proxy-bypass.js");
const releaseA = createDeferred();
const enteredA = createDeferred();
const callA = withNoProxyForLocalhost(async () => {
const callA = withNoProxyForLocalhostScoped(async () => {
expect(process.env.NO_PROXY).toContain("localhost");
expect(process.env.NO_PROXY).toContain("[::1]");
enteredA.resolve();
@@ -247,7 +248,7 @@ describe("withNoProxyForLocalhost concurrency", () => {
await enteredA.promise;
const callB = withNoProxyForLocalhost(async () => {
const callB = withNoProxyForLocalhostScoped(async () => {
return "b";
});
@@ -264,21 +265,22 @@ describe("withNoProxyForLocalhost concurrency", () => {
describe("withNoProxyForLocalhost reverse exit order", () => {
it("restores NO_PROXY when first caller exits before second", async () => {
await withIsolatedNoProxyEnv(async () => {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostItem } =
await import("./cdp-proxy-bypass.js");
const enteredA = createDeferred();
const enteredB = createDeferred();
const releaseA = createDeferred();
const releaseB = createDeferred();
const callA = withNoProxyForLocalhost(async () => {
const callA = withNoProxyForLocalhostItem(async () => {
enteredA.resolve();
await releaseA.promise;
return "a";
});
await enteredA.promise;
const callB = withNoProxyForLocalhost(async () => {
const callB = withNoProxyForLocalhostItem(async () => {
enteredB.resolve();
await releaseB.promise;
return "b";
@@ -306,9 +308,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostCandidate } =
await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
await withNoProxyForLocalhostCandidate(async () => {
// Should not modify since loopback is already covered
expect(process.env.NO_PROXY).toBe(userNoProxy);
return "ok";
@@ -332,9 +335,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostEntry } =
await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
await withNoProxyForLocalhostEntry(async () => {
expect(process.env.NO_PROXY).toBe(`${coveredNoProxy},localhost,127.0.0.1,[::1]`);
expect(process.env.no_proxy).toBe(`${staleLowerNoProxy},localhost,127.0.0.1,[::1]`);
});
@@ -355,9 +359,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostResult } =
await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
await withNoProxyForLocalhostResult(async () => {
expect(process.env.NO_PROXY).toBe(`${lowerNoProxy},localhost,127.0.0.1,[::1]`);
expect(process.env.no_proxy).toBe(`${lowerNoProxy},localhost,127.0.0.1,[::1]`);
});
@@ -378,9 +383,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostValue } =
await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
await withNoProxyForLocalhostValue(async () => {
expect(process.env.NO_PROXY).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
expect(process.env.no_proxy).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
delete process.env.no_proxy;
@@ -402,9 +408,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
const { withNoProxyForLocalhost: withNoProxyForLocalhostLocal } =
await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
await withNoProxyForLocalhostLocal(async () => {
expect(process.env.NO_PROXY).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
expect(process.env.no_proxy).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
});

View File

@@ -42,7 +42,9 @@ import { BrowserCdpEndpointBlockedError } from "./errors.js";
async function startWsServer() {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
return { wss, port, url: `ws://127.0.0.1:${port}/devtools/browser/TEST` };
}
@@ -55,7 +57,9 @@ describe("cdp.helpers internal", () => {
registerManagedProxyBrowserCdpBypassMock.mockReset();
registerManagedProxyBrowserCdpBypassMock.mockImplementation(() => undefined);
if (wss) {
await new Promise<void>((resolve) => wss?.close(() => resolve()));
await new Promise<void>((resolve) => {
wss?.close(() => resolve());
});
wss = null;
}
});
@@ -307,7 +311,9 @@ describe("cdp.helpers internal", () => {
cb(true);
},
});
await new Promise<void>((resolve) => wss?.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss?.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
let callbackCount = 0;
wss.on("connection", (socket) => {
@@ -341,7 +347,9 @@ describe("cdp.helpers internal", () => {
cb(false, 429, "too many requests");
},
});
await new Promise<void>((resolve) => wss?.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss?.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
await expect(

View File

@@ -397,7 +397,9 @@ type CdpSocketOptions = {
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function normalizeRetryCount(value: number | undefined, fallback: number): number {

View File

@@ -79,7 +79,9 @@ function replyToViewportCommandOrScreenshot(
async function startMockWsServer(handle: CdpReplyHandler) {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
wss.on("connection", (socket) => {
socket.on("message", (raw) => {
@@ -113,7 +115,9 @@ describe("cdp internal", () => {
afterEach(async () => {
if (wss) {
await new Promise<void>((resolve) => wss?.close(() => resolve()));
await new Promise<void>((resolve) => {
wss?.close(() => resolve());
});
wss = null;
}
});
@@ -1072,7 +1076,9 @@ describe("cdp internal", () => {
// in createTargetViaCdp — the bare-ws root triggers discovery.
const http = await import("node:http");
const wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wsServer.once("listening", () => resolve());
});
const wsPort = (wsServer.address() as { port: number }).port;
wsServer.on("connection", (socket) => {
socket.on("message", (raw) => {
@@ -1110,7 +1116,9 @@ describe("cdp internal", () => {
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => httpServer.listen(0, "127.0.0.1", () => resolve()));
await new Promise<void>((resolve) => {
httpServer.listen(0, "127.0.0.1", () => resolve());
});
const httpPort = (httpServer.address() as { port: number }).port;
try {
const out = await createTargetViaCdp({
@@ -1119,8 +1127,12 @@ describe("cdp internal", () => {
});
expect(out.targetId).toBe("T_BARE_WS");
} finally {
await new Promise<void>((resolve) => wsServer.close(() => resolve()));
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
await new Promise<void>((resolve) => {
wsServer.close(() => resolve());
});
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
}
});

View File

@@ -27,7 +27,9 @@ describe("cdp", () => {
const startWsServer = async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
await new Promise<void>((resolve) => {
wsServer?.once("listening", resolve);
});
return (wsServer.address() as { port: number }).port;
};
@@ -77,7 +79,9 @@ describe("cdp", () => {
res.statusCode = 404;
res.end("not found");
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
return (httpServer.address() as { port: number }).port;
};
@@ -85,14 +89,16 @@ describe("cdp", () => {
vi.unstubAllEnvs();
await new Promise<void>((resolve) => {
if (!httpServer) {
return resolve();
resolve();
return;
}
httpServer.close(() => resolve());
httpServer = null;
});
await new Promise<void>((resolve) => {
if (!wsServer) {
return resolve();
resolve();
return;
}
wsServer.close(() => resolve());
wsServer = null;
@@ -190,7 +196,9 @@ describe("cdp", () => {
res.statusCode = 404;
res.end("not found");
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
const httpPort = (httpServer.address() as AddressInfo).port;
await expect(
@@ -210,7 +218,9 @@ describe("cdp", () => {
heldSockets.push(socket);
// Hold the TCP connection open without completing the WebSocket handshake.
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
const port = (httpServer.address() as AddressInfo).port;
try {
@@ -507,7 +517,9 @@ describe("cdp", () => {
}
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const addr = server.address() as AddressInfo;
const created = await createTargetViaCdp({
@@ -516,8 +528,12 @@ describe("cdp", () => {
});
expect(created.targetId).toBe("ROOT_FALLBACK");
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});

View File

@@ -490,11 +490,11 @@ describe("chrome MCP page parsing", () => {
url: "about:blank",
type: "page",
});
expect(session.client.callTool).toHaveBeenCalledWith({
expect(session.client["callTool"]).toHaveBeenCalledWith({
name: "new_page",
arguments: { url: "about:blank", timeout: 5000 },
});
const callToolMock = session.client.callTool as unknown as ToolCallMock;
const callToolMock = session.client["callTool"] as unknown as ToolCallMock;
const callNames = callToolMock.mock.calls.map(([call]) => call.name);
expect(callNames).not.toContain("navigate_page");
});
@@ -917,7 +917,7 @@ describe("chrome MCP page parsing", () => {
// intentionally no timeoutMs
});
const callToolMock = session.client.callTool as unknown as ToolCallMock;
const callToolMock = session.client["callTool"] as unknown as ToolCallMock;
const navigateCall = callToolMock.mock.calls.find(
([call]) => call.name === "navigate_page",
)?.[0];

View File

@@ -137,7 +137,7 @@ async function diagnoseCdpHealthCommand(
if (settled) {
return;
}
let parsed: { id?: unknown; result?: unknown } | null = null;
let parsed: { id?: unknown; result?: unknown } | null;
try {
parsed = JSON.parse(rawDataToString(raw)) as { id?: unknown; result?: unknown };
} catch {

View File

@@ -194,8 +194,12 @@ async function withMockChromeCdpServer(params: {
const addr = server.address() as AddressInfo;
await params.run(`http://127.0.0.1:${addr.port}`);
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
}
@@ -952,9 +956,13 @@ describe("chrome.ts internal", () => {
it("resolves false when the direct-ws probe cannot connect", async () => {
// Bind a ws server and then close it, so connecting to it fails.
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await expect(
isChromeReachable(`ws://127.0.0.1:${port}/devtools/browser/GONE`, 50),
).resolves.toBe(false);
@@ -962,7 +970,9 @@ describe("chrome.ts internal", () => {
it("resolves true when the direct-ws handshake succeeds", async () => {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as { port: number }).port;
try {
// Direct /devtools/ WS URL — isChromeReachable goes through
@@ -972,7 +982,9 @@ describe("chrome.ts internal", () => {
isChromeReachable(`ws://127.0.0.1:${port}/devtools/browser/OK`, 500),
).resolves.toBe(true);
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
}
});
});
@@ -994,9 +1006,13 @@ describe("chrome.ts internal", () => {
// accepting ws upgrades — the canRunCdpHealthCommand probe will
// fire its 'error' handler during handshake.
const dead = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => dead.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
dead.once("listening", () => resolve());
});
const deadPort = (dead.address() as { port: number }).port;
await new Promise<void>((resolve) => dead.close(() => resolve()));
await new Promise<void>((resolve) => {
dead.close(() => resolve());
});
const server = createServer((req, res) => {
if (req.url === "/json/version") {
res.writeHead(200, { "Content-Type": "application/json" });
@@ -1009,14 +1025,18 @@ describe("chrome.ts internal", () => {
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
try {
const addr = server.address() as AddressInfo;
await expect(isChromeCdpReady(`http://127.0.0.1:${addr.port}`, 50, 10)).resolves.toBe(
false,
);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});

View File

@@ -42,14 +42,12 @@ async function startLoopbackCdpServer(): Promise<RunningServer> {
afterEach(async () => {
await Promise.all(
runningServers
.splice(0)
.map(
(server) =>
new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
),
),
runningServers.splice(0).map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
}),
),
);
});

View File

@@ -108,8 +108,12 @@ async function withMockChromeCdpServer(params: {
const addr = server.address() as AddressInfo;
await params.run(`http://127.0.0.1:${addr.port}`);
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
}
@@ -549,7 +553,9 @@ describe("browser chrome helpers", () => {
}),
).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
@@ -559,7 +565,7 @@ describe("browser chrome helpers", () => {
onConnection: (wss) => {
wss.on("connection", (ws) => {
ws.on("message", (raw) => {
let message: { id?: unknown; method?: unknown } | null = null;
let message: { id?: unknown; method?: unknown } | null;
try {
const text =
typeof raw === "string"
@@ -755,8 +761,12 @@ describe("browser chrome helpers", () => {
expect(diagnostic.wsUrl).toBe(wsOnlyBase);
expect(diagnostic.browser).toBe("Browserless/Mock");
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
@@ -785,12 +795,16 @@ describe("browser chrome helpers", () => {
);
// A real WS server accepts the handshake.
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as AddressInfo).port;
try {
await expect(isChromeReachable(`ws://127.0.0.1:${port}`, 500)).resolves.toBe(true);
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
}
});
@@ -811,7 +825,9 @@ describe("browser chrome helpers", () => {
}
});
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
const port = (wss.address() as AddressInfo).port;
try {
await expect(isChromeCdpReady(`ws://127.0.0.1:${port}`, 500, 500)).resolves.toBe(true);
@@ -820,7 +836,9 @@ describe("browser chrome helpers", () => {
);
expect(diagnostic.wsUrl).toBe(`ws://127.0.0.1:${port}`);
} finally {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
}
});

View File

@@ -519,7 +519,9 @@ export async function launchOpenClawChrome(
if (exists(localStatePath) && exists(preferencesPath)) {
break;
}
await new Promise((r) => setTimeout(r, CHROME_BOOTSTRAP_PREFS_POLL_MS));
await new Promise((r) => {
setTimeout(r, CHROME_BOOTSTRAP_PREFS_POLL_MS);
});
}
try {
bootstrap.kill("SIGTERM");
@@ -531,7 +533,9 @@ export async function launchOpenClawChrome(
if (bootstrap.exitCode != null) {
break;
}
await new Promise((r) => setTimeout(r, CHROME_BOOTSTRAP_EXIT_POLL_MS));
await new Promise((r) => {
setTimeout(r, CHROME_BOOTSTRAP_EXIT_POLL_MS);
});
}
}
@@ -577,7 +581,9 @@ export async function launchOpenClawChrome(
launchHttpReachable = true;
break;
}
await new Promise((r) => setTimeout(r, CHROME_LAUNCH_READY_POLL_MS));
await new Promise((r) => {
setTimeout(r, CHROME_LAUNCH_READY_POLL_MS);
});
}
if (!launchHttpReachable) {
@@ -682,7 +688,9 @@ export async function stopOpenClawChrome(
return;
}
const remainingMs = timeoutMs - (Date.now() - start);
await new Promise((r) => setTimeout(r, Math.max(1, Math.min(100, remainingMs))));
await new Promise((r) => {
setTimeout(r, Math.max(1, Math.min(100, remainingMs)));
});
}
try {

View File

@@ -37,7 +37,9 @@ describe("browser client fetch attachOnly diagnostics", () => {
socket.on("close", () => sockets.delete(socket));
socket.on("error", () => {});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const port = (server.address() as { port: number }).port;
const configPath = path.join(tempHome.home, ".openclaw", "openclaw.json");
await fs.writeFile(
@@ -78,7 +80,9 @@ describe("browser client fetch attachOnly diagnostics", () => {
for (const socket of sockets) {
socket.destroy();
}
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
});

View File

@@ -469,7 +469,7 @@ export function resolveProfile(
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
let cdpHost = resolved.cdpHost;
let cdpPort = profile.cdpPort ?? 0;
let cdpUrl = "";
let cdpUrl;
const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw";
const headless = profile.headless ?? resolved.headless;
const headlessSource =

View File

@@ -212,7 +212,9 @@ describe("pw-session ensurePageState", () => {
try {
handlers.get("download")?.[0]?.(download);
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => {
setImmediate(resolve);
});
expect(unhandled).toStrictEqual([]);
await expect(download.path?.()).rejects.toThrow("save failed");

View File

@@ -947,7 +947,9 @@ async function connectBrowser(cdpUrl: string, ssrfPolicy?: SsrFPolicy): Promise<
break;
}
const delay = resolveCdpConnectRetryDelayMs(attempt);
await new Promise((r) => setTimeout(r, delay));
await new Promise((r) => {
setTimeout(r, delay);
});
}
}
if (lastErr instanceof Error) {
@@ -1066,7 +1068,7 @@ async function findPageByTargetId(
const pages = await getAllPages(browser);
let resolvedViaCdp = false;
for (const page of pages) {
let tid: string | null = null;
let tid: string | null;
try {
tid = await pageTargetId(page);
resolvedViaCdp = true;
@@ -1170,7 +1172,7 @@ export async function getPageForTargetId(opts: {
}
function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
let sameMainFrame = false;
let sameMainFrame;
try {
sameMainFrame = request.frame() === page.mainFrame();
} catch {
@@ -1197,7 +1199,7 @@ function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
}
function isSubframeDocumentNavigationRequest(page: Page, request: Request): boolean {
let sameMainFrame = false;
let sameMainFrame;
try {
sameMainFrame = request.frame() === page.mainFrame();
} catch {

View File

@@ -581,7 +581,9 @@ export async function clickViaPlaywright(opts: {
abortPromise,
reconcileRemoteDialog,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
}
if (opts.doubleClick) {
await awaitActionWithAbort(

View File

@@ -107,7 +107,7 @@ describe("pw-tools-core", () => {
await fs.writeFile(uploadPath, "fixture", "utf8");
const canonicalUploadPath = await fs.realpath(uploadPath);
const fileChooser = { setFiles: vi.fn(async () => {}) };
const waitForEvent = vi.fn(async (eventValue: string, _opts: unknown) => fileChooser);
const waitForEvent = vi.fn(async (_eventValue: string, _opts: unknown) => fileChooser);
setPwToolsCoreCurrentPage({
waitForEvent,
keyboard: { press: vi.fn(async () => {}) },

View File

@@ -45,7 +45,9 @@ import type { BrowserRouteRegistrar } from "./types.js";
import { asyncBrowserRoute, jsonError, toStringOrEmpty } from "./utils.js";
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500] as const;

View File

@@ -86,10 +86,10 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "page errors",
collect: async ({ cdpUrl, targetId, pw }) =>
collect: async ({ cdpUrl, targetId: targetIdValue, pw }) =>
await pw.getPageErrorsViaPlaywright({
cdpUrl,
targetId,
targetId: targetIdValue,
clear,
}),
});
@@ -109,10 +109,10 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "network requests",
collect: async ({ cdpUrl, targetId, pw }) =>
collect: async ({ cdpUrl, targetId: targetIdLocal, pw }) =>
await pw.getNetworkRequestsViaPlaywright({
cdpUrl,
targetId,
targetId: targetIdLocal,
filter: normalizeOptionalString(filter),
clear,
}),

View File

@@ -34,7 +34,9 @@ export async function resolveTargetIdAfterNavigate(opts: {
const first = pickReplacement(await opts.listTabs());
currentTargetId = first.targetId;
if (first.shouldRetry) {
await new Promise((r) => setTimeout(r, opts.retryDelayMs ?? 800));
await new Promise((r) => {
setTimeout(r, opts.retryDelayMs ?? 800);
});
currentTargetId = pickReplacement(await opts.listTabs(), {
allowSingleTabFallback: true,
}).targetId;

View File

@@ -25,14 +25,14 @@ const {
stopKnownBrowserProfilesMock,
trackedTabCleanupMock,
} = vi.hoisted(() => {
const trackedTabCleanupMock = vi.fn();
const trackedTabCleanupMockLocal = vi.fn();
return {
ensureExtensionRelayForProfilesMock: vi.fn(async () => {}),
getPwAiModuleMock: vi.fn(),
isPwAiLoadedMock: vi.fn(() => false),
startTrackedBrowserTabCleanupTimerMock: vi.fn(() => trackedTabCleanupMock),
startTrackedBrowserTabCleanupTimerMock: vi.fn(() => trackedTabCleanupMockLocal),
stopKnownBrowserProfilesMock: vi.fn(async () => {}),
trackedTabCleanupMock,
trackedTabCleanupMock: trackedTabCleanupMockLocal,
};
});

View File

@@ -286,7 +286,9 @@ export function createProfileAvailability({
if (await isReachable(attemptTimeoutMs)) {
return;
}
await new Promise((r) => setTimeout(r, CDP_READY_AFTER_LAUNCH_POLL_MS));
await new Promise((r) => {
setTimeout(r, CDP_READY_AFTER_LAUNCH_POLL_MS);
});
}
throw new Error(
`Chrome CDP websocket for profile "${profile.name}" is not reachable after start. ${await describeCdpFailure(
@@ -306,7 +308,9 @@ export function createProfileAvailability({
} catch (err) {
lastError = err;
}
await new Promise((r) => setTimeout(r, CHROME_MCP_ATTACH_READY_POLL_MS));
await new Promise((r) => {
setTimeout(r, CHROME_MCP_ATTACH_READY_POLL_MS);
});
}
throw new BrowserProfileUnavailableError(formatChromeMcpAttachFailure(lastError));
};

View File

@@ -350,7 +350,9 @@ export function createProfileTabOps({
triggerManagedTabLimit(found.targetId);
return assignTabAlias({ profileState, tab: found, label: opts?.label });
}
await new Promise((r) => setTimeout(r, OPEN_TAB_DISCOVERY_POLL_MS));
await new Promise((r) => {
setTimeout(r, OPEN_TAB_DISCOVERY_POLL_MS);
});
}
triggerManagedTabLimit(createdViaCdp);
return assignTabAlias({

View File

@@ -264,7 +264,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
isTransportAvailable: (timeoutMs) => getDefaultContext().isTransportAvailable(timeoutMs),
isReachable: (timeoutMs, options) => getDefaultContext().isReachable(timeoutMs, options),
listTabs: () => getDefaultContext().listTabs(),
openTab: (url, opts) => getDefaultContext().openTab(url, opts),
openTab: (url, optsLocal) => getDefaultContext().openTab(url, optsLocal),
labelTab: (targetId, label) => getDefaultContext().labelTab(targetId, label),
focusTab: (targetId) => getDefaultContext().focusTab(targetId),
closeTab: (targetId) => getDefaultContext().closeTab(targetId),

View File

@@ -21,7 +21,9 @@ function isTransientStartupFetchError(error: unknown): boolean {
}
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function postStartWithRetry(params: {

View File

@@ -41,7 +41,9 @@ describe("browser control HTTP auth", () => {
if (!current) {
return;
}
await new Promise<void>((resolve) => current.close(() => resolve()));
await new Promise<void>((resolve) => {
current.close(() => resolve());
});
});
it("requires bearer auth for standalone browser HTTP routes", async () => {

View File

@@ -11,14 +11,14 @@ import {
type DescribeFn = ReturnType<typeof vi.fn>;
function makeDeps(
describe: DescribeFn,
describeCandidate: DescribeFn,
overrides?: {
normalizeBrowserScreenshot?: ReturnType<typeof vi.fn>;
saveMediaBuffer?: ReturnType<typeof vi.fn>;
},
) {
return {
describeImageFile: describe as never,
describeImageFile: describeCandidate as never,
normalizeBrowserScreenshot:
(overrides?.normalizeBrowserScreenshot as never) ??
(vi.fn(async (buffer: Buffer) => ({ buffer })) as never),
@@ -41,7 +41,7 @@ async function withTempImage<T>(fn: (filePath: string) => Promise<T>): Promise<T
describe("describeBrowserScreenshot", () => {
it("uses existing image understanding config with a browser screenshot prompt", async () => {
const describe = vi.fn().mockResolvedValue({
const describeEntry = vi.fn().mockResolvedValue({
text: "A login screen.",
provider: "openai",
model: "gpt-vision",
@@ -62,7 +62,7 @@ describe("describeBrowserScreenshot", () => {
activeModel: { provider: "anthropic", model: "claude-sonnet-4.6" },
mediaScope: { sessionKey: "agent:main:telegram:dm:123", channel: "telegram" },
},
makeDeps(describe),
makeDeps(describeEntry),
);
expect(result).toEqual({
@@ -71,7 +71,7 @@ describe("describeBrowserScreenshot", () => {
model: "gpt-vision",
decision: { outcome: "success" },
});
expect(describe).toHaveBeenCalledWith({
expect(describeEntry).toHaveBeenCalledWith({
filePath,
cfg: {
tools: {
@@ -92,7 +92,7 @@ describe("describeBrowserScreenshot", () => {
});
it("resizes screenshots before image understanding when image sanitization is configured", async () => {
const describe = vi.fn().mockResolvedValue({ text: "Small screenshot." });
const describeResult = vi.fn().mockResolvedValue({ text: "Small screenshot." });
const normalizeBrowserScreenshot = vi.fn(async () => ({
buffer: Buffer.from("small"),
contentType: "image/jpeg" as const,
@@ -106,7 +106,7 @@ describe("describeBrowserScreenshot", () => {
filePath,
imageSanitization: { maxDimensionPx: 800 },
},
makeDeps(describe, { normalizeBrowserScreenshot, saveMediaBuffer }),
makeDeps(describeResult, { normalizeBrowserScreenshot, saveMediaBuffer }),
);
});
@@ -114,11 +114,11 @@ describe("describeBrowserScreenshot", () => {
maxSide: 800,
});
expect(saveMediaBuffer).toHaveBeenCalledWith(Buffer.from("small"), "image/jpeg", "browser");
expect(describe.mock.calls[0][0].filePath).toBe("/tmp/resized.jpg");
expect(describeResult.mock.calls[0][0].filePath).toBe("/tmp/resized.jpg");
});
it("returns null when image understanding is skipped or not configured", async () => {
const describe = vi.fn().mockResolvedValue({
const describeValue = vi.fn().mockResolvedValue({
text: undefined,
decision: { outcome: "skipped" },
});
@@ -126,13 +126,13 @@ describe("describeBrowserScreenshot", () => {
await expect(
describeBrowserScreenshot(
{ cfg: { browser: {} }, filePath: "/tmp/screenshot.png" },
makeDeps(describe),
makeDeps(describeValue),
),
).resolves.toBeNull();
});
it("does not pass an incomplete active model to media understanding", async () => {
const describe = vi.fn().mockResolvedValue({ text: "ok" });
const describeLocal = vi.fn().mockResolvedValue({ text: "ok" });
await describeBrowserScreenshot(
{
@@ -144,10 +144,10 @@ describe("describeBrowserScreenshot", () => {
filePath: "/tmp/screenshot.png",
activeModel: { model: "missing-provider" },
},
makeDeps(describe),
makeDeps(describeLocal),
);
expect(describe.mock.calls[0][0].activeModel).toBeUndefined();
expect(describeLocal.mock.calls[0][0].activeModel).toBeUndefined();
});
});

View File

@@ -150,7 +150,7 @@ function formatDoctorLine(check: BrowserDoctorCheck): string {
async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, deep?: boolean) {
const checks: BrowserDoctorCheck[] = [];
let status: BrowserStatus | null = null;
let status: BrowserStatus | null;
try {
status = await fetchBrowserStatus(parent, profile);
@@ -495,8 +495,10 @@ export function registerBrowserManageCommands(
if (printJsonResult(parent, result)) {
return;
}
const tab = (result as { tab?: BrowserTab }).tab;
defaultRuntime.log(`labeled tab ${tab?.tabId ?? targetId} as ${tab?.label ?? label}`);
const tabValue = (result as { tab?: BrowserTab }).tab;
defaultRuntime.log(
`labeled tab ${tabValue?.tabId ?? targetId} as ${tabValue?.label ?? label}`,
);
});
});
@@ -555,7 +557,7 @@ export function registerBrowserManageCommands(
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const tab = await callBrowserRequest<BrowserTab>(
const tabLocal = await callBrowserRequest<BrowserTab>(
parent,
{
method: "POST",
@@ -565,11 +567,11 @@ export function registerBrowserManageCommands(
},
{ timeoutMs: BROWSER_MANAGE_REQUEST_TIMEOUT_MS },
);
if (printJsonResult(parent, tab)) {
if (printJsonResult(parent, tabLocal)) {
return;
}
defaultRuntime.log(
`opened: ${tab.url}\n${tab.tabId ? `tab: ${tab.tabId}\n` : ""}${tab.label ? `label: ${tab.label}\n` : ""}id: ${tab.targetId}`,
`opened: ${tabLocal.url}\n${tabLocal.tabId ? `tab: ${tabLocal.tabId}\n` : ""}${tabLocal.label ? `label: ${tabLocal.label}\n` : ""}id: ${tabLocal.targetId}`,
);
});
});

View File

@@ -171,7 +171,7 @@ export async function handleBrowserGatewayRequest({
}
const cfg = getRuntimeConfig();
let nodeTarget: NodeSession | null = null;
let nodeTarget: NodeSession | null;
try {
nodeTarget = resolveBrowserNodeTarget({
cfg,

View File

@@ -388,7 +388,6 @@ describe("canvas host", () => {
const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`;
const linkPath = path.join(a2uiRoot, linkName);
let createdBundle = false;
let createdLink = false;
try {
await fs.stat(bundlePath);
@@ -398,7 +397,6 @@ describe("canvas host", () => {
}
await fs.symlink(path.join(process.cwd(), "package.json"), linkPath);
createdLink = true;
try {
const res = await captureA2uiResponse(`${A2UI_PATH}/`);
@@ -421,9 +419,7 @@ describe("canvas host", () => {
expect(symlinkRes.status).toBe(404);
expect(symlinkRes.body).toBe("not found");
} finally {
if (createdLink) {
await fs.rm(linkPath, { force: true });
}
await fs.rm(linkPath, { force: true });
if (createdBundle) {
await fs.rm(bundlePath, { force: true });
}

View File

@@ -443,7 +443,9 @@ export async function createCanvasHostHandler(
}
}
if (wss) {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
}
},
};
@@ -528,9 +530,9 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
if (ownsHandler) {
await handler.close();
}
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
);
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
},
};
}

View File

@@ -121,7 +121,9 @@ describe("ClickClack gateway", () => {
await vi.waitFor(() => expect(mocks.client.websocket).toHaveBeenCalledTimes(1));
socket.emit("message", Buffer.from("{not json"));
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => {
setImmediate(resolve);
});
expect(runError).toBeUndefined();
expect(ctx.log?.warn).toHaveBeenCalledWith(
"[default] skipped malformed ClickClack websocket event",

View File

@@ -190,7 +190,9 @@ export async function startClickClackGatewayAccount(
socket.on("error", reject);
});
if (!ctx.abortSignal.aborted) {
await new Promise((resolve) => setTimeout(resolve, account.reconnectMs));
await new Promise((resolve) => {
setTimeout(resolve, account.reconnectMs);
});
}
}
ctx.setStatus({ accountId: account.accountId, running: false });

View File

@@ -790,7 +790,9 @@ async function waitForFile(filePath: string): Promise<string> {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
}
}
throw new Error(`timed out waiting for ${filePath}`);
@@ -838,10 +840,14 @@ describe("connectCodexAppServerEndpoint", () => {
await expect(
Promise.race([
probe,
new Promise((_, reject) => setTimeout(() => reject(new Error("probe timed out")), 500)),
new Promise((_, reject) => {
setTimeout(() => reject(new Error("probe timed out")), 500);
}),
]),
).resolves.toMatchObject([{ endpointId: "ws", ok: false }]);
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
});
it("rejects malformed stdio frames instead of throwing out of band", async () => {
@@ -930,7 +936,9 @@ describe("connectCodexAppServerEndpoint", () => {
);
await expect(supervisor.probeEndpoints()).resolves.toEqual([{ endpointId: "exits", ok: true }]);
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
await expect(supervisor.probeEndpoints()).resolves.toMatchObject([
{
endpointId: "exits",

View File

@@ -0,0 +1,11 @@
# OpenClaw Codex
Official OpenClaw provider and harness plugin for OpenAI Codex app-server integration. It exposes the Codex-managed GPT model catalog and the Codex runtime surfaces used by OpenClaw agents.
Install from OpenClaw:
```bash
openclaw plugin add @openclaw/codex
```
Use this plugin when you want OpenClaw to run Codex-backed model turns, media understanding, and prompt overlays through the Codex app-server harness.

View File

@@ -216,7 +216,7 @@ describe("codex plugin", () => {
it("enables the native hook relay for public Codex side questions", async () => {
const harness = createCodexAppServerAgentHarness({ pluginConfig: { appServer: {} } });
const runSideQuestion = harness.runSideQuestion;
const runSideQuestion = harness["runSideQuestion"];
const result = { text: "ok" };
runCodexAppServerSideQuestionMock.mockResolvedValueOnce(result);

View File

@@ -266,7 +266,7 @@ describe("codex provider", () => {
listModels: listTestCodexAppServerModels,
});
expect(client.close).toHaveBeenCalledTimes(1);
expect(client["close"]).toHaveBeenCalledTimes(1);
});
it("does not close an active shared app-server client during live discovery", async () => {
@@ -293,8 +293,8 @@ describe("codex provider", () => {
listModels: listTestCodexAppServerModels,
});
expect(activeClient.close).not.toHaveBeenCalled();
expect(discoveryClient.close).toHaveBeenCalledTimes(1);
expect(activeClient["close"]).not.toHaveBeenCalled();
expect(discoveryClient["close"]).toHaveBeenCalledTimes(1);
});
it("resolves arbitrary Codex app-server model ids as text-only until discovered", () => {

View File

@@ -337,7 +337,6 @@ export async function startCodexAttemptThread(params: {
if (startupClientForAbandonedRequestCleanup === failedClient) {
startupClientForAbandonedRequestCleanup = undefined;
}
attemptedClient = undefined;
if (attempt >= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS) {
embeddedAgentLog.warn(
"codex app-server connection closed during startup; retries exhausted",

View File

@@ -147,7 +147,7 @@ describe("Codex app-server attempt timeouts", () => {
}, 5);
});
},
operation: async () => new Promise<never>(() => undefined),
operation: async () => new Promise<never>(() => {}),
});
const rejected = expect(run).rejects.toThrow("codex app-server startup timed out");
@@ -164,7 +164,7 @@ describe("Codex app-server attempt timeouts", () => {
const run = withCodexStartupTimeout({
timeoutMs: 1_000,
signal: controller.signal,
operation: async () => new Promise<never>(() => undefined),
operation: async () => new Promise<never>(() => {}),
});
const rejected = expect(run).rejects.toThrow("codex app-server startup aborted");

View File

@@ -486,7 +486,7 @@ describe("CodexAppServerClient", () => {
clients.push(harness.client);
harness.client.addRequestHandler((request) => {
if (request.method === "item/tool/call") {
return new Promise<never>(() => undefined);
return new Promise<never>(() => {});
}
return undefined;
});

View File

@@ -114,7 +114,7 @@ describe("maybeCompactCodexAppServerSession", () => {
);
expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" });
expect(fake.client.addNotificationHandler).not.toHaveBeenCalled();
expect(fake.client["addNotificationHandler"]).not.toHaveBeenCalled();
expect(result.ok).toBe(true);
expect(result.compacted).toBe(false);
expect(result.result?.tokensBefore).toBe(123);

View File

@@ -179,6 +179,32 @@ describe("Codex app-server dynamic tool build", () => {
expect(resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
});
it("quarantines unreadable tool entries before Codex-specific filtering", async () => {
const messageTool = createRuntimeDynamicTool("message");
const sourceTools = new Proxy([messageTool] as RuntimeDynamicToolForTest[], {
get(target, property, receiver) {
if (property === "0") {
throw new Error("fuzzplugin tool entry getter exploded");
}
if (property === "1") {
return messageTool;
}
if (property === "length") {
return 2;
}
return Reflect.get(target, property, receiver);
},
});
setOpenClawCodingToolsFactoryForTests(() => sourceTools);
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
await expect(buildDynamicToolsForTest(params, workspaceDir)).resolves.toEqual([messageTool]);
});
it("limits Codex memory flush runs to managed read and write tools", async () => {
const factoryOptions: unknown[] = [];
setOpenClawCodingToolsFactoryForTests((options) => {

View File

@@ -2,6 +2,7 @@ import {
buildAgentHookContextChannelFields,
buildEmbeddedAttemptToolRunContext,
embeddedAgentLog,
filterProviderNormalizableTools,
isSubagentSessionKey,
normalizeAgentRuntimeTools,
resolveAttemptSpawnWorkspaceDir,
@@ -9,6 +10,7 @@ import {
resolveSandboxContext,
supportsModelTools,
type EmbeddedRunAttemptParams,
type RuntimeToolSchemaDiagnostic,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { isToolAllowed } from "openclaw/plugin-sdk/sandbox";
@@ -265,15 +267,19 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
},
});
toolBuildStages.mark("create-openclaw-coding-tools");
const preNormalizationDiagnostics: RuntimeToolSchemaDiagnostic[] = [];
const readableAllToolProjection = filterProviderNormalizableTools(allTools);
preNormalizationDiagnostics.push(...readableAllToolProjection.diagnostics);
const readableAllTools = [...readableAllToolProjection.tools];
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(
addSandboxShellDynamicToolsIfAvailable(
isCodexMemoryFlushRun(params)
? filterCodexMemoryFlushDynamicTools(allTools)
: filterCodexDynamicTools(allTools, input.pluginConfig),
allTools,
? filterCodexMemoryFlushDynamicTools(readableAllTools)
: filterCodexDynamicTools(readableAllTools, input.pluginConfig),
readableAllTools,
input,
),
allTools,
readableAllTools,
input,
);
toolBuildStages.mark("codex-filtering");
@@ -295,8 +301,25 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
onPreNormalizationSchemaDiagnostics: (diagnostics) =>
preNormalizationDiagnostics.push(...diagnostics),
});
toolBuildStages.mark("runtime-normalization");
if (preNormalizationDiagnostics.length > 0) {
embeddedAgentLog.warn(
`codex app-server quarantined ${preNormalizationDiagnostics.length} unsupported runtime tool schema${preNormalizationDiagnostics.length === 1 ? "" : "s"} before dynamic tool registration`,
{
runId: params.runId,
sessionId: params.sessionId,
diagnostics: preNormalizationDiagnostics.map((diagnostic) => ({
index: diagnostic.toolIndex,
tool: diagnostic.toolName,
violations: diagnostic.violations.slice(0, 12),
violationCount: diagnostic.violations.length,
})),
},
);
}
const summary = toolBuildStages.snapshot();
if (shouldWarnCodexDynamicToolBuildStageSummary(summary)) {
const phase = input.forceHeartbeatTool ? "registered-tools" : "runtime-tools";
@@ -308,7 +331,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
phase,
totalMs: summary.totalMs,
stages: summary.stages,
allToolCount: allTools.length,
allToolCount: readableAllTools.length,
codexFilteredToolCount: codexFilteredTools.length,
visionFilteredToolCount: visionFilteredTools.length,
filteredToolCount: filteredTools.length,

View File

@@ -194,7 +194,7 @@ describe("dynamic tool execution helpers", () => {
toolBridge: {
handleToolCall: vi.fn((_call, options) => {
capturedSignal = options?.signal;
return new Promise<never>(() => undefined);
return new Promise<never>(() => {});
}),
},
signal: new AbortController().signal,
@@ -230,7 +230,7 @@ describe("dynamic tool execution helpers", () => {
arguments: { action: "poll", sessionId: "process-session", timeout: 30_000 },
},
toolBridge: {
handleToolCall: vi.fn(() => new Promise<never>(() => undefined)),
handleToolCall: vi.fn(() => new Promise<never>(() => {})),
},
signal: new AbortController().signal,
timeoutMs: 1,

View File

@@ -1,15 +1,15 @@
import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core";
import {
onInternalDiagnosticEvent,
waitForDiagnosticEventsDrained,
type DiagnosticEventPayload,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness";
import {
HEARTBEAT_RESPONSE_TOOL_NAME,
embeddedAgentLog,
wrapToolWithBeforeToolCallHook,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
onInternalDiagnosticEvent,
waitForDiagnosticEventsDrained,
type DiagnosticEventPayload,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import {
initializeGlobalHookRunner,
resetGlobalHookRunner,
@@ -85,12 +85,6 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
}
return value as Record<string, unknown>;
}
function requireArray(value: unknown, label: string): Array<unknown> {
expect(Array.isArray(value), label).toBe(true);
return value as Array<unknown>;
}
function callArg(
mock: { mock: { calls: Array<Array<unknown>> } },
callIndex: number,
@@ -346,9 +340,7 @@ describe("createCodexDynamicToolBridge", () => {
}),
);
const blockedEvents = diagnosticEvents.filter(
(
event,
): event is Extract<DiagnosticEventPayload, { type: "tool.execution.blocked" }> =>
(event): event is Extract<DiagnosticEventPayload, { type: "tool.execution.blocked" }> =>
event.type === "tool.execution.blocked",
);
expect(blockedEvents).toContainEqual(
@@ -889,7 +881,7 @@ describe("createCodexDynamicToolBridge", () => {
it("passes raw tool failure state into agent tool result middleware", async () => {
const registry = createEmptyPluginRegistry();
const handler = vi.fn(async (eventValue: { isError?: boolean }) => undefined);
const handler = vi.fn(async (_eventValue: { isError?: boolean }) => undefined);
registry.agentToolResultMiddlewares.push({
pluginId: "tokenjuice",
pluginName: "Tokenjuice",

View File

@@ -35,7 +35,9 @@ const tinyPngBase64 =
type ProjectorNotification = Parameters<CodexAppServerEventProjector["handleNotification"]>[0];
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => setImmediate(resolve));
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
}
function assistantMessage(text: string, timestamp: number) {
@@ -316,6 +318,29 @@ describe("CodexAppServerEventProjector", () => {
expect(result.replayMetadata.replaySafe).toBe(true);
});
it("streams final-answer assistant deltas into partial replies", async () => {
const { onPartialReply, projector } = await createProjectorWithAssistantHooks();
await projector.handleNotification(
forCurrentTurn("item/started", {
item: {
type: "agentMessage",
id: "msg-final",
phase: "final_answer",
text: "",
},
}),
);
await projector.handleNotification(agentMessageDelta("hel", "msg-final"));
await projector.handleNotification(agentMessageDelta("lo", "msg-final"));
expect(onPartialReply).toHaveBeenCalledTimes(2);
expect(onPartialReply.mock.calls.map((call) => call[0])).toEqual([
{ text: "hel", delta: "hel" },
{ text: "hello", delta: "lo" },
]);
});
it("suppresses mirrored user prompt when the inbound message was already persisted", async () => {
const params = await createParams();
const projector = await createProjector({

View File

@@ -447,6 +447,11 @@ export class CodexAppServerEventProjector {
if (!delta) {
return;
}
this.rememberAssistantPhase(readItem(params.item));
const phase = readString(params, "phase");
if (phase) {
this.assistantPhaseByItem.set(itemId, phase);
}
if (!this.assistantStarted) {
this.assistantStarted = true;
await this.params.onAssistantMessageStart?.();
@@ -456,10 +461,13 @@ export class CodexAppServerEventProjector {
this.assistantTextByItem.set(itemId, text);
if (this.isCommentaryAssistantItem(itemId)) {
this.emitCommentaryProgress({ itemId, text });
} else if (this.shouldStreamAssistantPartial(itemId)) {
await this.params.onPartialReply?.({ text, delta });
}
// Codex app-server can emit multiple agentMessage items per turn, including
// intermediate coordination/progress prose. Keep those deltas internal until
// turn completion chooses the last assistant item as the user-visible reply.
// their phase identifies terminal answer text or turn completion chooses the
// last assistant item as the user-visible reply.
}
private async handleReasoningDelta(
@@ -970,6 +978,10 @@ export class CodexAppServerEventProjector {
return this.assistantPhaseByItem.get(itemId) === "commentary";
}
private shouldStreamAssistantPartial(itemId: string): boolean {
return this.assistantPhaseByItem.get(itemId) === "final_answer";
}
private emitCommentaryProgress(params: { itemId: string; text: string }): void {
const progressText = params.text.replace(/\s+/g, " ").trim();
if (

View File

@@ -52,8 +52,30 @@ function createRuntime() {
error?: string;
}>;
};
const createRunningTaskRun = vi.fn(
(params): AgentHarnessTaskRecord => ({
taskId: params.sourceId ?? params.runId,
runtime: "subagent",
sourceId: params.sourceId,
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
agentId: params.agentId,
runId: params.runId,
label: params.label,
task: params.task,
status: "running",
deliveryStatus: params.deliveryStatus ?? "not_applicable",
notifyPolicy: params.notifyPolicy ?? "silent",
createdAt: params.startedAt ?? Date.now(),
startedAt: params.startedAt,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
}),
);
const taskRuntime = {
createRunningTaskRun: vi.fn(),
createRunningTaskRun,
tryCreateRunningTaskRun: vi.fn((params) => createRunningTaskRun(params)),
recordTaskRunProgressByRunId: vi.fn(() => []),
finalizeTaskRunByRunId: vi.fn(() => []),
listTaskRecords: vi.fn((): AgentHarnessTaskRecord[] => []),

View File

@@ -7,7 +7,7 @@ import {
function createRuntime() {
return {
createRunningTaskRun: vi.fn(),
tryCreateRunningTaskRun: vi.fn((params) => ({ taskId: "task-native-subagent", ...params })),
recordTaskRunProgressByRunId: vi.fn(() => []),
finalizeTaskRunByRunId: vi.fn(() => []),
} as unknown as TaskLifecycleRuntime;
@@ -49,7 +49,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith({
sourceId: "codex-thread:child-thread",
agentId: "main",
runId: "codex-thread:child-thread",
@@ -62,7 +62,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
lastEventAt: 20_000,
progressSummary: "Codex native subagent started.",
});
expect(vi.mocked(runtime.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
"childSessionKey",
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
@@ -99,7 +99,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.createRunningTaskRun).not.toHaveBeenCalled();
expect(runtime.tryCreateRunningTaskRun).not.toHaveBeenCalled();
expect(runtime.recordTaskRunProgressByRunId).not.toHaveBeenCalled();
expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
});
@@ -133,7 +133,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
mirror.handleNotification(notification);
mirror.handleNotification(notification);
expect(runtime.createRunningTaskRun).toHaveBeenCalledTimes(1);
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledTimes(1);
});
it("maps Codex thread status changes onto the mirrored task run", () => {
@@ -228,7 +228,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith({
sourceId: "codex-thread:child-thread",
runId: "codex-thread:child-thread",
label: "Codex subagent",
@@ -240,7 +240,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
lastEventAt: 40_000,
progressSummary: "Codex native subagent spawned.",
});
expect(vi.mocked(runtime.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
"childSessionKey",
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
@@ -282,7 +282,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",
@@ -319,7 +319,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",

View File

@@ -15,7 +15,7 @@ import { isJsonObject } from "./protocol.js";
export type TaskLifecycleRuntime = Pick<
AgentHarnessTaskRuntime,
"createRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
"tryCreateRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
>;
export type CodexNativeSubagentTaskMirrorParams = {
@@ -27,6 +27,7 @@ export type CodexNativeSubagentTaskMirrorParams = {
export class CodexNativeSubagentTaskMirror {
private readonly mirroredThreadIds = new Set<string>();
private readonly failedMirrorThreadIds = new Set<string>();
private readonly terminalRunIds = new Set<string>();
private readonly now: () => number;
@@ -81,7 +82,7 @@ export class CodexNativeSubagentTaskMirror {
trimOptional(thread.preview) ??
`Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`;
const createdAt = secondsToMillis(thread.createdAt) ?? this.now();
this.runtime.createRunningTaskRun({
const taskRecord = this.runtime.tryCreateRunningTaskRun({
sourceId: runId,
agentId: this.params.agentId,
runId,
@@ -94,6 +95,13 @@ export class CodexNativeSubagentTaskMirror {
lastEventAt: this.now(),
progressSummary: "Codex native subagent started.",
});
if (!taskRecord) {
this.mirroredThreadIds.delete(threadId);
this.failedMirrorThreadIds.add(threadId);
return;
}
this.failedMirrorThreadIds.delete(threadId);
this.terminalRunIds.delete(runId);
this.applyStatus(threadId, thread.status);
}
@@ -106,6 +114,9 @@ export class CodexNativeSubagentTaskMirror {
}
private applyStatus(threadId: string, status: CodexThreadStatus | null | undefined): void {
if (!this.mirroredThreadIds.has(threadId) && this.failedMirrorThreadIds.has(threadId)) {
return;
}
const statusType = status?.type;
if (!statusType) {
return;
@@ -219,7 +230,7 @@ export class CodexNativeSubagentTaskMirror {
const prompt = trimOptional(readString(item, "prompt"));
const runId = codexNativeSubagentRunId(normalizedThreadId);
const createdAt = this.now();
this.runtime.createRunningTaskRun({
const taskRecord = this.runtime.tryCreateRunningTaskRun({
sourceId: runId,
agentId: this.params.agentId,
runId,
@@ -232,6 +243,13 @@ export class CodexNativeSubagentTaskMirror {
lastEventAt: createdAt,
progressSummary: "Codex native subagent spawned.",
});
if (!taskRecord) {
this.mirroredThreadIds.delete(normalizedThreadId);
this.failedMirrorThreadIds.add(normalizedThreadId);
return;
}
this.failedMirrorThreadIds.delete(normalizedThreadId);
this.terminalRunIds.delete(runId);
}
private applyCollabAgentStatus(
@@ -239,6 +257,9 @@ export class CodexNativeSubagentTaskMirror {
status: string | undefined,
message: string | null | undefined,
): void {
if (!this.mirroredThreadIds.has(threadId) && this.failedMirrorThreadIds.has(threadId)) {
return;
}
const normalizedStatus = normalizeAgentStateStatus(status);
if (!normalizedStatus) {
return;

View File

@@ -85,7 +85,9 @@ async function drainActiveAppServerAttemptsForTest(): Promise<void> {
}
await Promise.race([
Promise.allSettled(attempts.map((attempt) => attempt.promise)),
new Promise<void>((resolve) => setTimeout(resolve, 5_000)),
new Promise<void>((resolve) => {
setTimeout(resolve, 5_000);
}),
]);
}
@@ -312,7 +314,7 @@ export function createAppServerHarness(
return {
request,
requests,
async waitForMethod(method: string, timeoutMs: number = appServerHarnessWait.timeout) {
waitForMethod: async (method: string, timeoutMs: number = appServerHarnessWait.timeout) => {
await vi.waitFor(
() => {
if (!requests.some((entry) => entry.method === method)) {
@@ -330,15 +332,15 @@ export function createAppServerHarness(
{ interval: 1, timeout: timeoutMs },
);
},
async notify(notification: CodexServerNotification) {
notify: async (notification: CodexServerNotification) => {
await sendNotification(notification);
},
waitForServerRequestHandler,
async handleServerRequest(request: Parameters<AppServerRequestHandler>[0]) {
handleServerRequest: async (requestLocal: Parameters<AppServerRequestHandler>[0]) => {
const handler = await waitForServerRequestHandler();
return handler(request);
return handler(requestLocal);
},
async completeTurn(params: { threadId: string; turnId: string }) {
completeTurn: async (params: { threadId: string; turnId: string }) => {
await sendNotification({
method: "turn/completed",
params: {
@@ -348,7 +350,7 @@ export function createAppServerHarness(
},
});
},
close() {
close: () => {
for (const handler of closeHandlers) {
handler();
}

View File

@@ -334,16 +334,17 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
if (!contextEngine.bootstrap) {
throw new Error("expected bootstrap hook");
}
expect(contextEngine.bootstrap).toHaveBeenCalledTimes(1);
const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters<
NonNullable<ContextEngine["bootstrap"]>
>[0];
expect(contextEngine["bootstrap"]).toHaveBeenCalledTimes(1);
const bootstrapParams = requireFirstCallArg(
contextEngine["bootstrap"],
"bootstrap",
) as Parameters<NonNullable<ContextEngine["bootstrap"]>>[0];
expect(bootstrapParams.sessionId).toBe("session-1");
expect(bootstrapParams.sessionKey).toBe("agent:main:session-1");
expect(bootstrapParams.sessionFile).toBe(sessionFile);
expect(contextEngine.assemble).toHaveBeenCalledTimes(1);
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
expect(contextEngine["assemble"]).toHaveBeenCalledTimes(1);
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
ContextEngine["assemble"]
>[0];
expect(assembleParams.sessionId).toBe("session-1");
@@ -385,12 +386,13 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
if (!contextEngine.bootstrap) {
throw new Error("expected bootstrap hook");
}
const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters<
NonNullable<ContextEngine["bootstrap"]>
>[0];
const bootstrapParams = requireFirstCallArg(
contextEngine["bootstrap"],
"bootstrap",
) as Parameters<NonNullable<ContextEngine["bootstrap"]>>[0];
expect(bootstrapParams.sessionKey).toBe("agent:main:main");
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
ContextEngine["assemble"]
>[0];
expect(assembleParams.sessionKey).toBe("agent:main:main");
@@ -1640,7 +1642,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
await harness.completeTurn();
await run;
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
ContextEngine["assemble"]
>[0];
expect(assembleParams.messages.map((message) => message.role)).toEqual([

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