Compare commits

..

155 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
980 changed files with 21303 additions and 8916 deletions

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
@@ -49,8 +49,8 @@ aws:
region: eu-west-1
rootGB: 400
azure:
# The OpenClaw Azure subscription has reliable D2 spot capacity in eastus2;
# eastus rejects the same SKUs and can stall provisioning.
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
# same SKUs and can stall provisioning.
location: eastus2
sync:
delete: true
@@ -71,14 +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
type: Standard_D2ads_v6
market: spot
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
@@ -95,8 +97,8 @@ jobs:
provider: azure
target: linux
class: standard
type: Standard_D2ads_v6
market: spot
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
@@ -105,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

@@ -22,6 +22,7 @@
"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",
@@ -35,6 +36,7 @@
"eslint/no-useless-constructor": "error",
"eslint/no-useless-rename": "error",
"eslint/no-useless-return": "error",
"eslint/no-useless-assignment": "error",
"eslint/no-unused-vars": "error",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",

View File

@@ -57,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

@@ -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

@@ -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

@@ -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

@@ -76,8 +76,7 @@ Notes:
extra approval scopes:
- commandless request: `operator.pairing`
- non-exec command request: `operator.pairing` + `operator.write`
- `system.run` / `system.run.prepare` / `system.which` /
`system.execApprovals.*` request:
- `system.run` / `system.run.prepare` / `system.which` request:
`operator.pairing` + `operator.admin`
<Warning>

View File

@@ -417,13 +417,6 @@ Nodes must advertise `system.execApprovals.get/set` (macOS app or
headless node host). If a node does not advertise exec approvals yet,
edit its local `~/.openclaw/exec-approvals.json` directly.
Some node hosts, including native Windows hosts, expose host-native
approval snapshots instead of a file-backed OpenClaw approvals file. The
Control UI shows those snapshots as read-only because the native host owns
the policy format and editor. Use the Windows companion app or
`openclaw approvals set --node <id|name|ip>` for supported updates on
those nodes.
CLI: `openclaw approvals` supports gateway or node editing - see
[Approvals CLI](/cli/approvals).

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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);

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

@@ -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

@@ -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

@@ -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) {

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);
}),
]);
}

View File

@@ -66,7 +66,9 @@ describe("runCodexAppServerAttempt hooks and model diagnostics", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
expect(llmInput).toHaveBeenCalled();
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
const [llmInputPayload, llmInputContext] = mockCall(llmInput, "llm_input") as [
{

View File

@@ -817,7 +817,7 @@ describe("runCodexAppServerAttempt", () => {
onTimeout: async () => {
await releaseCodexSandboxExecServerEnvironment(sandbox);
},
operation: async () => new Promise<never>(() => undefined),
operation: async () => new Promise<never>(() => {}),
}),
).rejects.toThrow("codex app-server startup timed out");
@@ -1111,7 +1111,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -1684,7 +1686,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1725,7 +1729,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1762,7 +1768,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1801,7 +1809,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -1846,7 +1856,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -1891,7 +1903,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -2134,7 +2148,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2189,7 +2205,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2266,7 +2284,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2454,7 +2474,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2483,7 +2505,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2526,7 +2550,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2566,7 +2592,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2609,7 +2637,9 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2851,7 +2881,9 @@ describe("runCodexAppServerAttempt", () => {
const result = await run;
expect(result.aborted).toBe(true);
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => {
setImmediate(resolve);
});
expect(unhandledRejections).toStrictEqual([]);
} finally {
process.off("unhandledRejection", onUnhandledRejection);
@@ -2943,7 +2975,9 @@ describe("runCodexAppServerAttempt", () => {
{ turnTerminalIdleTimeoutMs: 60_000 },
);
await bufferedTerminal;
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
harness.close();
const result = await run;
@@ -2983,7 +3017,9 @@ describe("runCodexAppServerAttempt", () => {
turnTerminalIdleTimeoutMs: 60_000,
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -3076,7 +3112,9 @@ describe("runCodexAppServerAttempt", () => {
},
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(resolved).toBe(false);
await harness.notify({
@@ -3120,7 +3158,9 @@ describe("runCodexAppServerAttempt", () => {
},
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(resolved).toBe(false);
expect(
warn.mock.calls.some(([message]) =>
@@ -3800,7 +3840,7 @@ describe("runCodexAppServerAttempt", () => {
});
it("times out app-server startup before thread setup can hang forever", async () => {
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => undefined));
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => {}));
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
@@ -3834,7 +3874,9 @@ describe("runCodexAppServerAttempt", () => {
interval: 1,
});
await waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -4307,7 +4349,7 @@ describe("runCodexAppServerAttempt", () => {
const c = {
request: vi.fn(async (method: string) => {
if (method === "thread/start") {
return await new Promise<never>(() => undefined);
return await new Promise<never>(() => {});
}
return {};
}),
@@ -4502,7 +4544,9 @@ describe("runCodexAppServerAttempt", () => {
interval: 1,
});
await waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;

View File

@@ -965,8 +965,19 @@ export async function runCodexAppServerAttempt(
let client: CodexAppServerClient;
let thread: CodexAppServerThreadLifecycleBinding;
let trajectoryEndRecorded = false;
const markTrajectoryEndRecorded = () => {
trajectoryEndRecorded = true;
};
let nativeHookRelay: NativeHookRelayRegistrationHandle | undefined;
let releaseSharedClientLease: (() => void) | undefined;
const releaseSharedClientLeaseOnce = () => {
const release = releaseSharedClientLease;
if (!release) {
return;
}
releaseSharedClientLease = undefined;
release();
};
let sandboxExecEnvironmentAcquired = false;
const releaseSandboxExecEnvironment = async () => {
if (sandboxExecEnvironmentAcquired) {
@@ -1914,7 +1925,7 @@ export async function runCodexAppServerAttempt(
aborted: runAbortController.signal.aborted,
promptError: turnStartErrorMessage,
});
trajectoryEndRecorded = true;
markTrajectoryEndRecorded();
runAgentHarnessLlmOutputHook({
event: {
runId: params.runId,
@@ -1979,8 +1990,7 @@ export async function runCodexAppServerAttempt(
},
});
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
releaseSharedClientLeaseOnce();
if (usageLimitError) {
await markCodexAuthProfileBlockedFromRateLimits({
params,
@@ -2000,8 +2010,7 @@ export async function runCodexAppServerAttempt(
}
}
if (!turn) {
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
releaseSharedClientLeaseOnce();
throw new Error("codex app-server turn/start failed without an error");
}
turnIdRef.current = turn.turn.id;
@@ -2250,7 +2259,7 @@ export async function runCodexAppServerAttempt(
yieldDetected,
promptError: normalizeCodexTrajectoryError(finalPromptError),
});
trajectoryEndRecorded = true;
markTrajectoryEndRecorded();
await mirrorTranscriptBestEffort({
params,
agentId: sessionAgentId,
@@ -2427,7 +2436,7 @@ export async function runCodexAppServerAttempt(
notificationCleanup();
requestCleanup();
closeCleanup?.();
releaseSharedClientLease?.();
releaseSharedClientLeaseOnce();
if (nativeHookRelay) {
if (shouldDelayNativeHookRelayUnregister) {
// Codex hook subprocesses can outlive a completed app-server turn by a

View File

@@ -71,7 +71,9 @@ describe("createCodexAttemptTurnWatchController", () => {
try {
controller.armAttemptIdleWatch();
controller.touchActivity("turn:start", { attemptProgress: true });
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
controller.noteNotificationReceived("response.output_text.delta", {
attemptProgress: true,
attemptTimeoutMs: 40,
@@ -405,7 +407,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
return turnStartResult("turn-1", "inProgress");
}
if (method === "turn/interrupt") {
return new Promise<never>(() => undefined);
return new Promise<never>(() => {});
}
return {};
});
@@ -474,7 +476,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await harness.notify({
method: "rawResponseItem/completed",
params: {
@@ -488,7 +492,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await harness.notify({
method: "rawResponseItem/completed",
params: {
@@ -543,7 +549,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await harness.handleServerRequest({
id: "request-account-refresh",
method: "account/nonTurnRefresh",
@@ -595,7 +603,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
void harness.handleServerRequest({
id: "request-auth-refresh",
method: "account/chatgptAuthTokens/refresh",
@@ -659,7 +669,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await harness.handleServerRequest({
id: "request-null-turn-elicitation",
method: "mcpServer/elicitation/request",
@@ -673,7 +685,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
_meta: null,
},
});
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -735,7 +749,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
),
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
expect(
onRunProgress.mock.calls.some(
([event]) =>
@@ -788,7 +804,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 75));
await new Promise((resolve) => {
setTimeout(resolve, 75);
});
const response = harness.handleServerRequest({
id: "request-user-input",
method: "item/tool/requestUserInput",
@@ -812,7 +830,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1), fastWait);
await new Promise((resolve) => setTimeout(resolve, 125));
await new Promise((resolve) => {
setTimeout(resolve, 125);
});
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
expect(queueActiveRunMessageForTest("session-1", "2")).toBe(true);
@@ -843,7 +863,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => setTimeout(resolve, 60));
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await harness.handleServerRequest({
id: "request-foreign-elicitation",
method: "mcpServer/elicitation/request",
@@ -1052,7 +1074,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1158,7 +1182,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1258,7 +1284,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await notify({
@@ -1342,7 +1370,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
})) as { success?: boolean };
expect(toolResult.success).toBe(false);
await new Promise((resolve) => setTimeout(resolve, 130));
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1406,7 +1436,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 130));
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
expect(settled).toBe(false);
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1486,7 +1518,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => setTimeout(resolve, 130));
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1679,7 +1713,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(settled).toBe(false);
const result = await run;
@@ -1793,7 +1829,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 40));
await new Promise((resolve) => {
setTimeout(resolve, 40);
});
expect(settled).toBe(false);
const result = await run;
@@ -1884,7 +1922,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
// This covers the future-compatible path for raw response deltas if Codex
// app-server exposes them directly; current Codex primarily emits
// rawResponseItem/completed for the raw-event surface.
@@ -1896,7 +1936,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
delta: '{"cmd":"apply_patch","patch":"large chunk"}',
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
expect(settled).toBe(false);
await notify({
@@ -1989,7 +2031,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await notify({
method: "item/fileChange/patchUpdated",
params: {
@@ -2096,7 +2140,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await notify({
method: "response.custom_tool_call_input.delta",
params: {
@@ -2194,7 +2240,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 40));
await new Promise((resolve) => {
setTimeout(resolve, 40);
});
await notify({
method: "response.custom_tool_call_input.delta",
params: {
@@ -2597,7 +2645,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 25));
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2650,7 +2700,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 25));
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2686,7 +2738,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 100));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2740,7 +2794,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => setTimeout(resolve, 100));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2763,7 +2819,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 15 });
await harness.waitForMethod("turn/start");
await harness.notify(rateLimitsUpdated(Date.now() + 60_000));
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
const result = await run;
expect({
@@ -2880,7 +2938,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
const queuedTerminal = harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
void queuedTerminal.catch(() => undefined);
await new Promise((resolve) => setTimeout(resolve, 30));
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
expect(settled).toBe(false);
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -3191,7 +3251,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3272,7 +3334,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3433,7 +3497,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3677,7 +3743,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
harness.close();
const result = await run;
@@ -3745,7 +3813,9 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(resolved).toBe(false);
await harness.notify({

View File

@@ -206,7 +206,9 @@ export async function waitForHttpBodyDeltas(
if (deltas.length >= count) {
return deltas;
}
await new Promise((resolve) => setTimeout(resolve, 25));
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
}
throw new Error(`expected ${count} http body deltas`);
}

View File

@@ -704,7 +704,9 @@ describe("shared Codex app-server client", () => {
});
try {
await new Promise<void>((resolve) => server.once("listening", resolve));
await new Promise<void>((resolve) => {
server.once("listening", resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected websocket test server port");
@@ -741,9 +743,9 @@ describe("shared Codex app-server client", () => {
expect(authHeaders).toEqual(["Bearer tok-first", "Bearer tok-second"]);
} finally {
clearSharedCodexAppServerClient();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
});

View File

@@ -141,7 +141,9 @@ function mockCall(mock: ReturnType<typeof vi.fn>, index = 0): unknown[] {
}
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => setImmediate(resolve));
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
}
function activeDiagnosticToolKeys(events: DiagnosticEventPayload[]): Set<string> {

View File

@@ -12,14 +12,12 @@ describe("Codex app-server websocket transport", () => {
}
clients.length = 0;
await Promise.all(
servers
.splice(0)
.map(
(server) =>
new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
),
),
servers.splice(0).map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
),
);
});
@@ -42,7 +40,9 @@ describe("Codex app-server websocket transport", () => {
}
});
});
await new Promise<void>((resolve) => server.once("listening", resolve));
await new Promise<void>((resolve) => {
server.once("listening", resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected websocket test server port");

View File

@@ -1096,7 +1096,9 @@ describe("codex conversation binding", () => {
},
{ timeoutMs: 50 },
);
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(result).toEqual({
handled: true,

View File

@@ -387,7 +387,9 @@ function isCodexPluginLoadWarningItem(item: MigrationItem): boolean {
}
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function buildTargetCodexPluginAppCacheKey(ctx: MigrationProviderContext): Promise<string> {

View File

@@ -440,7 +440,9 @@ async function waitForLocalHistory(params: {
}
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
}
}
@@ -479,7 +481,9 @@ async function waitForCloudCompletion(params: {
}
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
}
}

View File

@@ -55,7 +55,9 @@ function createDeferred<T>() {
}
async function flushAsyncWork() {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
describe("createCopilotAgentHarness", () => {

View File

@@ -729,7 +729,9 @@ describe("runCopilotAttempt", () => {
});
const session = await sessionCreated.promise;
for (let i = 0; i < 100 && session.sendAndWait.mock.calls.length === 0; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
expect(session.sendAndWait).toHaveBeenCalledTimes(1);

View File

@@ -420,7 +420,7 @@ export function convertOpenClawToolToSdkTool(
);
}
let preparedArgs = args;
let preparedArgs;
try {
preparedArgs = sourceTool.prepareArguments ? sourceTool.prepareArguments(args) : args;
} catch (error: unknown) {

View File

@@ -346,7 +346,9 @@ async function emitAndCaptureLog(
}
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => setImmediate(resolve));
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
}
function emitTrustedModelCallCompletedWithContent(
@@ -3297,24 +3299,26 @@ describe("diagnostics-otel service", () => {
},
{
inputMessages: [
{ role: "user", content: "what changed?", timestamp: 1 },
{
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "lookup", arguments: { q: "trace" } }],
},
{ role: "toolResult", toolCallId: "call-1", content: { rows: 1 } },
],
{ role: "user", content: "what changed?", timestamp: 1 },
{
role: "assistant",
content: [
{ type: "toolCall", id: "call-1", name: "lookup", arguments: { q: "trace" } },
],
},
{ role: "toolResult", toolCallId: "call-1", content: { rows: 1 } },
],
outputMessages: [
{
role: "assistant",
content: [{ type: "text", text: "the trace changed" }],
stopReason: "stop",
},
],
{
role: "assistant",
content: [{ type: "text", text: "the trace changed" }],
stopReason: "stop",
},
],
systemPrompt: "be exact",
toolDefinitions: [
{ name: "lookup", description: "Lookup data", parameters: { type: "object" } },
],
{ name: "lookup", description: "Lookup data", parameters: { type: "object" } },
],
},
);
await flushDiagnosticEvents();

View File

@@ -478,8 +478,8 @@ export function normalizeCompatibilityConfig({
}
const changes: string[] = [];
let updated = rawEntry;
let changed = false;
let updated;
let changed;
const bindingsToAdd: AgentBindingConfig[] = [];
const aliases = normalizeLegacyChannelAliases({

View File

@@ -298,7 +298,7 @@ export function createDiscordAutoPresenceController(params: {
let lastAppliedAt = 0;
const runEvaluation = (options?: { force?: boolean }) => {
let decision: DiscordAutoPresenceDecision | null = null;
let decision: DiscordAutoPresenceDecision | null;
try {
decision = resolveDiscordAutoPresenceDecision({
discordConfig: params.discordConfig,

View File

@@ -256,7 +256,7 @@ export function createDiscordDraftPreviewController(params: {
);
}
const alreadyStarted = progressDraftGate.hasStarted;
let progressActive = false;
let progressActive;
if (shouldStartDiscordProgressDraftNow(line)) {
await progressDraftGate.startNow();
progressActive = progressDraftGate.hasStarted;

View File

@@ -146,7 +146,7 @@ function copyRuntimeMessageFields(source: Message, target: Message): void {
}
function shouldHydrateDiscordMessage(params: { message: Message }) {
let currentText = "";
let currentText;
try {
currentText = resolveDiscordMessageText(params.message, {
includeForwarded: true,

View File

@@ -1154,9 +1154,13 @@ describe("processDiscordMessage ack reactions", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onCompactionStart?.();
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
await params?.replyOptions?.onCompactionEnd?.();
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
return createNoQueuedDispatchResult();
});
@@ -1545,7 +1549,9 @@ describe("processDiscordMessage session routing", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onReasoningStream?.();
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
return createNoQueuedDispatchResult();
});
const ctx = await createBaseContext({
@@ -1583,7 +1589,9 @@ describe("processDiscordMessage session routing", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onReasoningStream?.();
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
return createNoQueuedDispatchResult();
});
const ctx = await createBaseContext({

View File

@@ -119,7 +119,9 @@ export async function applyDiscordModelPickerSelection(params: {
const fallbackRoute = dispatchResult.effectiveRoute ?? params.route;
if (params.settleMs > 0) {
await new Promise((resolve) => setTimeout(resolve, params.settleMs));
await new Promise((resolve) => {
setTimeout(resolve, params.settleMs);
});
}
let effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
@@ -135,7 +137,9 @@ export async function applyDiscordModelPickerSelection(params: {
params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime,
});
await new Promise((resolve) => setTimeout(resolve, 100));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
}
@@ -155,7 +159,9 @@ export async function applyDiscordModelPickerSelection(params: {
params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime,
});
await new Promise((resolve) => setTimeout(resolve, 100));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
if (!persisted) {

View File

@@ -336,7 +336,7 @@ export function formatDiscordDeployErrorDetails(err: unknown): string {
details.push(`code=${discordCode}`);
}
if (rawBody !== undefined) {
let bodyText = "";
let bodyText;
try {
bodyText = JSON.stringify(rawBody);
} catch {

View File

@@ -440,7 +440,9 @@ describe("createDiscordGatewayPlugin", () => {
process.on("unhandledRejection", onUnhandledRejection);
try {
startIgnoredGatewayRegistration(plugin);
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => {
setImmediate(resolve);
});
expect(unhandledReasons).toHaveLength(0);
const registration = waitForDiscordGatewayPluginRegistration(plugin);

View File

@@ -3865,14 +3865,18 @@ describe("DiscordVoiceManager", () => {
resolveSecond?.({ payloads: [{ text: "second answer" }] });
resolveThird?.({ payloads: [{ text: "third answer" }] });
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expectUserMessageNotIncludes("second answer");
expectUserMessageNotIncludes("third answer");
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const firstStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(firstStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expectUserMessageNotIncludes("second answer");
const idleHandler = player.on.mock.calls.find(([event]) => event === "idle")?.[1] as
@@ -3886,7 +3890,9 @@ describe("DiscordVoiceManager", () => {
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const secondStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(secondStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expectUserMessageNotIncludes("third answer");
idleHandler?.();
@@ -3950,7 +3956,9 @@ describe("DiscordVoiceManager", () => {
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const firstStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(firstStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expectUserMessageNotIncludes("second answer");
const idleHandler = player.on.mock.calls.find(([event]) => event === "idle")?.[1] as

View File

@@ -538,7 +538,9 @@ async function waitForFalQueueResult(params: {
throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
}
const pollDelayMs = resolveFalQueueRemainingMs(params.deadline, lastStatus, POLL_INTERVAL_MS);
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
}
}

View File

@@ -340,7 +340,9 @@ export async function getAppOwnerOpenId(params: {
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function sleepRegistrationPollInterval(intervalSeconds: number): Promise<void> {

View File

@@ -92,7 +92,7 @@ export function resolveFeishuGroupSession(params: {
(replyInThread ? messageId : null))
: null;
let peerId = chatId;
let peerId;
switch (groupSessionScope) {
case "group_sender":
peerId = buildFeishuConversationId({ chatId, scope: "group_sender", senderOpenId });

View File

@@ -64,7 +64,7 @@ export const detectFeishuLegacyStateMigrations: BundledChannelLegacyStateMigrati
stateDir,
}) => {
const dedupDir = path.join(stateDir, "feishu", "dedup");
let entries: fs.Dirent[] = [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dedupDir, { withFileTypes: true });
} catch {

View File

@@ -400,7 +400,7 @@ function inspectSessionTranscript(params: {
return null;
}
let raw = "";
let raw;
try {
raw = fs.readFileSync(params.transcriptPath, "utf-8");
} catch {

View File

@@ -474,7 +474,7 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
}
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null = null;
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null | undefined;
try {
const eventDispatcher = createEventDispatcher(account);
const chatHistories = new Map<string, HistoryEntry[]>();

View File

@@ -361,7 +361,9 @@ async function resolveParsedCommentContent(params: {
}
async function delayMs(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function buildDriveCommentTargetUrl(params: {

View File

@@ -1,5 +1,9 @@
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import {
fetchWithSsrFGuard,
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { vi } from "vitest";
import type { ClawdbotConfig } from "../runtime-api.js";
import type { monitorFeishuProvider } from "./monitor.js";
@@ -10,26 +14,41 @@ const WEBHOOK_MONITOR_START_MAX_ATTEMPTS = 4;
export async function getFreePort(): Promise<number> {
const server = createServer();
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 address = server.address() as AddressInfo | null;
if (!address) {
throw new Error("missing server address");
}
await new Promise<void>((resolve) => server.close(() => resolve()));
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
return address.port;
}
async function waitUntilServerReady(url: string): Promise<void> {
for (let i = 0; i < WEBHOOK_READY_MAX_ATTEMPTS; i += 1) {
try {
const response = await fetch(url, { method: "GET" });
if (response.status >= 200 && response.status < 500) {
return;
const { response, release } = await fetchWithSsrFGuard({
url,
init: { method: "GET" },
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
auditContext: "feishu-webhook-test-ready",
});
try {
if (response.status >= 200 && response.status < 500) {
return;
}
} finally {
await release();
}
} catch {
// retry
}
await new Promise((resolve) => setTimeout(resolve, WEBHOOK_READY_RETRY_DELAY_MS));
await new Promise((resolve) => {
setTimeout(resolve, WEBHOOK_READY_RETRY_DELAY_MS);
});
}
throw new Error(`server did not start: ${url}`);
}
@@ -108,7 +127,9 @@ export async function withRunningWebhookMonitor(
abortController.abort();
await monitorPromise.catch(() => undefined);
if (attempt < WEBHOOK_MONITOR_START_MAX_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, attempt * WEBHOOK_READY_RETRY_DELAY_MS));
await new Promise((resolve) => {
setTimeout(resolve, attempt * WEBHOOK_READY_RETRY_DELAY_MS);
});
}
}
}

View File

@@ -162,6 +162,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
resolveMarkdownTableMode: vi.fn(() => "preserve"),
convertMarkdownTables: vi.fn((text) => text),
chunkTextWithMode: vi.fn((text) => [text]),
chunkMarkdownTextWithMode: vi.fn((text) => [text]),
},
reply: {
createReplyDispatcherWithTyping: createReplyDispatcherWithTypingMock,
@@ -403,6 +404,85 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
it("keeps oversized auto mode plain final text on the chunked message path", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["0123456789", "abcdefghij"]);
const { options } = createDispatcherHarness();
await options.deliver({ text: "0123456789abcdefghij" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(0);
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendMessageFeishuMock, "first message send params", {
text: "0123456789",
});
expectMockArgFields(
sendMessageFeishuMock,
"second message send params",
{
text: "abcdefghij",
},
1,
);
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
});
it("keeps oversized auto mode markdown final text on the chunked card path", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkMarkdownTextWithMode.mockReturnValue(["```ts\nx\n```", "tail"]);
const { options } = createDispatcherHarness({ runtime: createRuntimeLogger() });
await options.deliver({ text: "```ts\nconst x = 1\n```\ntail" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(0);
expect(runtime.channel.text.chunkMarkdownTextWithMode).toHaveBeenCalledTimes(1);
expect(runtime.channel.text.chunkTextWithMode).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendStructuredCardFeishuMock, "first card send params", {
text: "```ts\nx\n```",
});
expectMockArgFields(
sendStructuredCardFeishuMock,
"second card send params",
{
text: "tail",
},
1,
);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
});
it("discards partial streaming preview before oversized final text fallback", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["final text", " overflow"]);
const { result, options } = createDispatcherHarness({ runtime: createRuntimeLogger() });
result.replyOptions.onPartialReply?.({ text: "partial" });
await options.deliver({ text: "final text overflow" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).not.toHaveBeenCalled();
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendMessageFeishuMock, "first message send params", {
text: "final text",
});
expectMockArgFields(
sendMessageFeishuMock,
"second message send params",
{
text: " overflow",
},
1,
);
});
it("keeps auto mode plain tool text on the message path when streaming is enabled", async () => {
const { options } = createDispatcherHarness();
await options.deliver({ text: "tool summary" }, { kind: "tool" });
@@ -760,6 +840,33 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
});
it("skips oversized late final text after streaming card close", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["oversized ", "late final"]);
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
await options.deliver({ text: "First" }, { kind: "final" });
await options.onIdle?.();
await options.deliver(
{ text: "oversized late final", mediaUrl: "https://example.com/a.png" },
{ kind: "final" },
);
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
expectMockArgFields(sendMediaFeishuMock, "media send params", {
mediaUrl: "https://example.com/a.png",
});
});
it("suppresses duplicate final text while still sending media", async () => {
const options = setupNonStreamingAutoDispatcher();
await options.deliver({ text: "plain final" }, { kind: "final" });
@@ -1698,7 +1805,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
const fallbackPromise = result.ensureNoVisibleReplyFallback("zero-final-count");
for (let attempt = 0; attempt < 20 && closeMock.mock.calls.length === 0; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
expect(closeMock).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();

View File

@@ -463,9 +463,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
const chunkSource = paramsLocal.useCard
? paramsLocal.text
: core.channel.text.convertMarkdownTables(paramsLocal.text, tableMode);
const chunkText = paramsLocal.useCard
? core.channel.text.chunkMarkdownTextWithMode
: core.channel.text.chunkTextWithMode;
const chunks = resolveTextChunksWithFallback(
chunkSource,
core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode),
chunkText(chunkSource, textChunkLimit, chunkMode),
);
for (const [index, chunk] of chunks.entries()) {
await paramsLocal.sendChunk({
@@ -629,13 +632,21 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
}),
);
const streamingCardEnabledForReplyKind = streamingEnabled && info?.kind === "final";
const useCard =
const finalTextExceedsStreamingLimit =
info?.kind === "final" && hasText && text.length > textChunkLimit;
const useStaticCard =
hasText &&
(streamingCardEnabledForReplyKind ||
renderMode === "card" ||
(renderMode === "card" ||
(info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw") ||
(renderMode === "auto" && shouldUseCard(text)));
const useStreamingCard =
hasText &&
streamingEnabled &&
!finalTextExceedsStreamingLimit &&
(info?.kind === "final" || useStaticCard);
const finalTextWouldUseStreamingCard =
info?.kind === "final" && hasText && streamingEnabled;
const useCard = useStaticCard || useStreamingCard;
const skipTextForDuplicateFinal =
info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
const skipTextForClosedStreamingFinal =
@@ -643,8 +654,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
hasText &&
streamingClosedForReply &&
!streamingCloseErroredForReply &&
streamingEnabled &&
useCard;
finalTextWouldUseStreamingCard;
const shouldDeliverText =
hasText &&
!hasVoiceMedia &&
@@ -652,8 +662,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
!skipTextForClosedStreamingFinal;
const shouldDiscardStreamingPreview =
info?.kind === "final" &&
hasMedia &&
((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal);
(finalTextExceedsStreamingLimit ||
(hasMedia && ((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal)));
if (!shouldDeliverText && !hasMedia) {
return;
@@ -667,7 +677,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
if (info?.kind === "block") {
// Drop internal block chunks unless we can safely consume them as
// streaming-card fallback content.
if (!(streamingEnabled && useCard)) {
if (!useStreamingCard) {
return;
}
startStreaming();
@@ -676,7 +686,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
}
}
if (info?.kind === "final" && streamingEnabled && useCard) {
if (info?.kind === "final" && useStreamingCard) {
startStreaming();
if (streamingStartPromise) {
await streamingStartPromise;

View File

@@ -85,7 +85,9 @@ describe("createSequentialQueue", () => {
}),
).rejects.toThrow("boom");
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(unhandled).toStrictEqual([]);
await expect(enqueue("feishu:default:chat-1", async () => {})).resolves.toBeUndefined();

View File

@@ -347,7 +347,7 @@ async function runNewAppFlow(params: {
const targetAccountId = resolveDefaultFeishuAccountId(next);
// ----- QR scan flow -----
let appId: string | null = null;
let appId: string | null;
let appSecret: SecretInput | null = null;
let appSecretProbeValue: string | null = null;
let scanDomain: FeishuDomain | undefined;
@@ -366,7 +366,6 @@ async function runNewAppFlow(params: {
if (scanResult) {
appId = scanResult.appId;
appSecret = scanResult.appSecret;
appSecretProbeValue = scanResult.appSecret;
scanDomain = scanResult.domain;
scanOpenId = scanResult.openId;
} else {
@@ -421,7 +420,9 @@ async function runNewAppFlow(params: {
// ----- Apply credentials & security policy -----
const configProgress = prompter.progress(t("wizard.feishu.configuring"));
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
if (appId && appSecret) {
next = patchFeishuConfig(next, targetAccountId, {

View File

@@ -588,7 +588,7 @@ export function createDirFetchTool(): AnyAgentTool {
throw new Error(`dir.fetch UNCOMPRESSED_TOO_LARGE: ${reason}`);
};
for (const { relPath, absPath } of walked) {
let size = 0;
let size;
try {
const st = await fs.stat(absPath);
size = st.size;

View File

@@ -60,6 +60,10 @@ type GithubCopilotTestProvider = {
catalog: {
run: (ctx: unknown) => Promise<ProviderCatalogResult>;
};
resolveThinkingProfile: (ctx: {
modelId?: string;
compat?: { supportedReasoningEfforts?: readonly string[] };
}) => { levels: Array<{ id: string }> };
};
type GithubCopilotTestModelCatalogProvider = {
liveCatalog: (ctx: unknown) => Promise<readonly UnifiedModelCatalogEntry[] | null | undefined>;
@@ -180,6 +184,17 @@ describe("github-copilot plugin", () => {
expect(mocks.resolveCopilotApiToken).not.toHaveBeenCalled();
});
it("exposes xhigh thinking for catalog-supported Copilot reasoning efforts", () => {
const provider = registerProviderWithPluginConfig({});
const profile = provider.resolveThinkingProfile({
modelId: "claude-opus-4.7-1m-internal",
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
});
expect(profile.levels.map((level) => level.id)).toContain("xhigh");
});
it("uses live plugin config to re-enable discovery after startup disable", async () => {
mocks.resolveCopilotApiToken.mockResolvedValueOnce({
token: "copilot_api_token",

View File

@@ -42,6 +42,17 @@ type GithubCopilotPluginConfig = {
};
};
function compatSupportsXHigh(
compat: { supportedReasoningEfforts?: readonly string[] } | null | undefined,
) {
return (
Array.isArray(compat?.supportedReasoningEfforts) &&
compat.supportedReasoningEfforts.some(
(effort) => normalizeOptionalLowercaseString(effort) === "xhigh",
)
);
}
async function loadGithubCopilotRuntime() {
return await import("./register.runtime.js");
}
@@ -450,20 +461,22 @@ export default definePluginEntry({
resolveDynamicModel: (ctx) => resolveCopilotForwardCompatModel(ctx),
wrapStreamFn: wrapCopilotProviderStream,
buildReplayPolicy: ({ modelId }) => buildGithubCopilotReplayPolicy(modelId),
resolveThinkingProfile: ({ modelId }) => ({
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
...(COPILOT_XHIGH_MODEL_IDS.includes(
resolveThinkingProfile: ({ modelId, compat }) => {
const modelSupportsXHigh =
COPILOT_XHIGH_MODEL_IDS.includes(
(normalizeOptionalLowercaseString(modelId) ?? "") as never,
)
? [{ id: "xhigh" as const }]
: []),
],
}),
) || compatSupportsXHigh(compat);
return {
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
...(modelSupportsXHigh ? [{ id: "xhigh" as const }] : []),
],
};
},
prepareRuntimeAuth: async (ctx) => {
const { resolveCopilotApiToken } = await loadGithubCopilotRuntime();
const token = await resolveCopilotApiToken({

View File

@@ -221,7 +221,9 @@ async function sleepGitHubDevicePollDelay(delayMs: number, expiresAt: number): P
while (Date.now() < targetAt) {
const remainingMs = Math.max(1, targetAt - Date.now());
const safeDelayMs = resolveTimerTimeoutMs(remainingMs, 1);
await new Promise((resolve) => setTimeout(resolve, Math.min(safeDelayMs, remainingMs)));
await new Promise((resolve) => {
setTimeout(resolve, Math.min(safeDelayMs, remainingMs));
});
}
}

View File

@@ -11,6 +11,29 @@ const COPILOT_CHAT_COMPLETIONS_COMPAT: ModelDefinitionConfig["compat"] = {
};
const STATIC_MODEL_OVERRIDES = new Map<string, Partial<ModelDefinitionConfig>>([
[
"claude-opus-4.6-1m",
{
name: "Claude Opus 4.6 (1M context)",
api: "anthropic-messages",
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 64_000,
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
},
],
[
"claude-opus-4.7-1m-internal",
{
name: "Claude Opus 4.7 (1M context)",
api: "anthropic-messages",
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 64_000,
thinkingLevelMap: { xhigh: "xhigh" },
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
},
],
[
"gpt-5.5",
{

View File

@@ -48,6 +48,9 @@ export function buildCopilotModelDefinition(modelId: string): ModelDefinitionCon
cost: staticOverride?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: staticOverride?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: staticOverride?.maxTokens ?? DEFAULT_MAX_TOKENS,
...(staticOverride?.thinkingLevelMap
? { thinkingLevelMap: staticOverride.thinkingLevelMap }
: {}),
...(compat ? { compat } : {}),
};
}

View File

@@ -104,6 +104,22 @@ describe("github-copilot model defaults", () => {
});
});
it("uses static metadata overrides for Claude Opus 1M fallback rows", () => {
const def = buildCopilotModelDefinition("claude-opus-4.7-1m-internal");
expect(def).toEqual({
id: "claude-opus-4.7-1m-internal",
name: "Claude Opus 4.7 (1M context)",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 64_000,
thinkingLevelMap: { xhigh: "xhigh" },
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
});
});
it("trims whitespace from model id", () => {
const def = buildCopilotModelDefinition(" gpt-4o ");
expect(def.id).toBe("gpt-4o");
@@ -205,6 +221,14 @@ describe("resolveCopilotForwardCompatModel", () => {
});
});
it("preserves static Anthropic thinking maps for Claude Opus 1M fallback rows", () => {
const result = requireResolvedModel(createMockCtx("claude-opus-4.7-1m-internal"));
expect(result.thinkingLevelMap).toEqual({ xhigh: "xhigh" });
expect(result.compat).toEqual({
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
});
});
it("creates synthetic model for arbitrary unknown model ID", () => {
const ctx = createMockCtx("gpt-5.4-mini");
const result = requireResolvedModel(ctx);
@@ -476,7 +500,11 @@ describe("fetchCopilotModelCatalog", () => {
max_context_window_tokens: 1000000,
max_output_tokens: 64000,
},
supports: { vision: true, tool_calls: true },
supports: {
vision: true,
tool_calls: true,
reasoning_effort: ["low", "medium", "high", "xhigh"],
},
},
},
{
@@ -540,6 +568,7 @@ describe("fetchCopilotModelCatalog", () => {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 400000,
maxTokens: 128000,
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
});
const codex = out.find((m) => m.id === "gpt-5.3-codex");
@@ -559,6 +588,10 @@ describe("fetchCopilotModelCatalog", () => {
const opus1m = out.find((m) => m.id === "claude-opus-4.7-1m-internal");
expect(opus1m?.api).toBe("anthropic-messages");
expect(opus1m?.contextWindow).toBe(1_000_000);
expect(opus1m?.thinkingLevelMap).toEqual({ xhigh: "xhigh" });
expect(opus1m?.compat).toEqual({
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
});
});
it("strips trailing slash from baseUrl when building the /models URL", async () => {

View File

@@ -77,6 +77,9 @@ export function resolveCopilotForwardCompatModel(
cost: staticOverride.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: staticOverride.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: staticOverride.maxTokens ?? DEFAULT_MAX_TOKENS,
...(staticOverride.thinkingLevelMap
? { thinkingLevelMap: staticOverride.thinkingLevelMap }
: {}),
...(compat ? { compat } : {}),
} as ProviderRuntimeModel);
}
@@ -145,6 +148,41 @@ function resolveCopilotApiForVendor(
return resolveCopilotTransportApi(modelId);
}
function mergeCopilotCompat(
base: ModelDefinitionConfig["compat"] | undefined,
reasoningEfforts: string[] | null | undefined,
): ModelDefinitionConfig["compat"] | undefined {
const supportedReasoningEfforts = Array.isArray(reasoningEfforts)
? [
...new Set(
reasoningEfforts
.map((effort) => normalizeOptionalLowercaseString(effort))
.filter((effort): effort is string => Boolean(effort)),
),
]
: [];
if (supportedReasoningEfforts.length === 0) {
return base;
}
return {
...base,
supportedReasoningEfforts,
};
}
function resolveCopilotThinkingLevelMap(
api: ModelDefinitionConfig["api"],
compat: ModelDefinitionConfig["compat"] | undefined,
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
if (
api === "anthropic-messages" &&
compat?.supportedReasoningEfforts?.some((effort) => effort === "xhigh")
) {
return { xhigh: "xhigh" };
}
return undefined;
}
function mapCopilotApiModelToDefinition(
entry: CopilotApiModelEntry,
): ModelDefinitionConfig | undefined {
@@ -174,17 +212,20 @@ function mapCopilotApiModelToDefinition(
const contextWindow =
asPositiveSafeInteger(limits?.max_context_window_tokens) ?? DEFAULT_CONTEXT_WINDOW;
const maxTokens = asPositiveSafeInteger(limits?.max_output_tokens) ?? DEFAULT_MAX_TOKENS;
const compat = resolveCopilotModelCompat(id);
const compat = mergeCopilotCompat(resolveCopilotModelCompat(id), supports?.reasoning_effort);
const api = resolveCopilotApiForVendor(entry.vendor, id);
const thinkingLevelMap = resolveCopilotThinkingLevelMap(api, compat);
const definition: ModelDefinitionConfig = {
id,
name: entry.name?.trim() || id,
api: resolveCopilotApiForVendor(entry.vendor, id),
api,
reasoning,
input,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow,
maxTokens,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
...(compat ? { compat } : {}),
};
return definition;

View File

@@ -140,7 +140,9 @@ function startGoogleMeetNodeAudioInputLoop(params: {
if (consecutiveInputErrors >= 5 || /unknown bridgeId|bridge is not open/i.test(message)) {
await params.stop();
} else {
await new Promise((resolve) => setTimeout(resolve, 250));
await new Promise((resolve) => {
setTimeout(resolve, 250);
});
}
}
}

View File

@@ -766,7 +766,9 @@ async function openMeetWithBrowserRequest(params: {
}
const remainingWaitMs = deadline - Date.now();
if (remainingWaitMs > 0) {
await new Promise((resolve) => setTimeout(resolve, Math.min(750, remainingWaitMs)));
await new Promise((resolve) => {
setTimeout(resolve, Math.min(750, remainingWaitMs));
});
}
} while (Date.now() < deadline);
return { launched: true, browser };

View File

@@ -86,6 +86,16 @@ describe("google generative ai helpers", () => {
});
it("normalizes transport baseUrls only for Google Generative AI", () => {
expect(
resolveGoogleGenerativeAiTransport({
provider: "google",
api: undefined,
baseUrl: "https://generativelanguage.googleapis.com",
}),
).toEqual({
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
});
expect(
resolveGoogleGenerativeAiTransport({
api: "google-generative-ai",

View File

@@ -267,7 +267,9 @@ async function waitForGeminiBatch(params: {
throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`);
}
params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`);
await new Promise((resolve) => setTimeout(resolve, params.pollIntervalMs));
await new Promise((resolve) => {
setTimeout(resolve, params.pollIntervalMs);
});
current = undefined;
}
}

View File

@@ -64,7 +64,9 @@ async function pollOperation(
headers: Record<string, string>,
): Promise<{ done?: boolean; response?: { cloudaicompanionProject?: { id?: string } } }> {
for (let attempt = 0; attempt < 24; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 5000));
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
const response = await fetchWithTimeout(`${endpoint}/v1internal/${operationName}`, {
headers,
});

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