Compare commits

..

156 Commits

Author SHA1 Message Date
Vincent Koc
f2f893c14a refactor(agents): extract code mode runtime package 2026-05-30 14:55:20 +01:00
Peter Steinberger
f44af7eebf fix(gateway): guard live probe schedule timestamps 2026-05-30 09:52:20 -04:00
Peter Steinberger
65fe2b7e91 ci: tolerate release branches without llm core package 2026-05-30 14:48:08 +01:00
Peter Steinberger
941e04e9f3 fix: clamp configured OpenAI-compatible output tokens 2026-05-30 14:46:30 +01:00
AI-HUB
f327073fb3 fix: classify ws pre-handshake close as benign
Classify the exact `ws` pre-handshake close-before-open error as a benign uncaught network exception so transient Feishu WebSocket cleanup does not crash the gateway process.

The classifier now keeps the upstream `ws` message as an exact contract and rejects broader prefixed WebSocket messages, with regression coverage for direct, wrapped, and non-exact cases.

Fixes #88257.
Thanks @akrimm702.

Co-authored-by: AI-HUB <144416483+akrimm702@users.noreply.github.com>
2026-05-30 15:45:23 +02:00
Peter Steinberger
41e5acbb6c perf(gateway): skip unchanged auth persistence writes 2026-05-30 14:44:45 +01:00
Peter Steinberger
2333d47a1e fix(matrix): guard verification timestamps 2026-05-30 09:43:09 -04:00
Vincent Koc
c9e481ac48 refactor: share approval request registration 2026-05-30 15:40:49 +02:00
scotthuang
462e315953 fix(ui): stop pulsing completed stream segments
Completed WebChat stream segment bubbles now render without the active streaming animation after live output has moved on. The UI chat item contract now marks completed stream segments as non-streaming and the active stream as streaming, so the renderer applies the pulsing class only to live output.

Verified with:
- node scripts/run-vitest.mjs ui/src/ui/chat/build-chat-items.test.ts ui/src/ui/chat/grouped-render.test.ts ui/src/ui/views/chat.test.ts
- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-ui-stream-artifacts.tsbuildinfo
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

PR: #88225
Credit: @scotthuang
2026-05-30 15:40:12 +02:00
Peter Steinberger
6b14df7792 fix(qqbot): guard token expiry logging 2026-05-30 09:38:58 -04:00
Vincent Koc
e449392c4f fix(e2e): route telegram proof through pnpm runner 2026-05-30 15:33:38 +02:00
Peter Steinberger
326db58229 fix(gateway): guard hook job timestamps 2026-05-30 09:33:19 -04:00
Vincent Koc
3caf4facec fix(test): include workflow lint target in routing expectation (#88310) 2026-05-30 14:29:26 +01:00
Peter Steinberger
c9a97f54e0 fix(discord): preserve preference recency under invalid clocks 2026-05-30 09:29:02 -04:00
Vincent Koc
85506c36a0 fix(e2e): route secret proof through pnpm runner 2026-05-30 15:25:15 +02:00
Ayaan Zaidi
a176b8ec2f perf(cli): compact resumed room-event prompts 2026-05-30 18:53:59 +05:30
Ayaan Zaidi
2b726457d8 fix(cli): persist first room-event session binding 2026-05-30 18:53:59 +05:30
Vincent Koc
6464f8d1d9 refactor: share visible approval list mapping 2026-05-30 15:19:10 +02:00
Peter Steinberger
a17c7a56da fix(sessions): guard transcript append timestamps 2026-05-30 09:08:20 -04:00
Peter Steinberger
98a1aa491f fix(gateway): guard lock payload timestamps 2026-05-30 09:04:34 -04:00
Vincent Koc
25b87b111d refactor: share find tool result builder 2026-05-30 15:00:22 +02:00
Peter Steinberger
f823123aa5 fix(time): centralize date timestamp fallback 2026-05-30 08:59:36 -04:00
Vincent Koc
d717ff71bf fix(live): reject loose heartbeat intervals 2026-05-30 14:56:58 +02:00
Peter Steinberger
840192caa9 fix(diffs): cap artifact expiry overflow 2026-05-30 08:54:56 -04:00
Vincent Koc
61ef6b12dd test(agents): harden code mode wait timeout 2026-05-30 13:53:25 +01:00
Peter Steinberger
660a6dec7f fix(cron): reject out-of-range cli relative times 2026-05-30 08:52:47 -04:00
Peter Steinberger
e49ef86945 fix(cron): guard timestamp validation clocks 2026-05-30 08:49:58 -04:00
Peter Steinberger
d2f69ecc3b fix(migrate): guard report timestamp formatting 2026-05-30 08:46:55 -04:00
Vincent Koc
a89abcb1e9 fix(release): reject loose npm verifier retry limits 2026-05-30 14:46:28 +02:00
Peter Steinberger
8bf7bc5b5c fix(sessions): guard archive timestamp formatting 2026-05-30 08:43:22 -04:00
Vincent Koc
4e2ef87c31 refactor: share git url parsing helpers 2026-05-30 14:42:17 +02:00
Vincent Koc
ec58491f75 fix(e2e): reject loose upgrade probe limits 2026-05-30 14:40:12 +02:00
Peter Steinberger
0840fea50d fix(matrix): guard startup verification timestamps 2026-05-30 08:38:12 -04:00
Vincent Koc
cf60e83118 fix(e2e): scope strict ClawHub preflight limits 2026-05-30 14:33:56 +02:00
Peter Steinberger
7ad2ebb515 fix(google): guard realtime browser session expiries 2026-05-30 08:33:06 -04:00
Peter Steinberger
3c41e1722f fix(discord): guard timeout expiry dates 2026-05-30 08:29:15 -04:00
Vincent Koc
dd5b70bcc4 refactor: share web search provider load context 2026-05-30 14:25:30 +02:00
Peter Steinberger
30c0422a8e fix(commitments): guard extraction prompt timestamps 2026-05-30 08:24:27 -04:00
Vincent Koc
6d43200248 fix(e2e): reject loose Telegram proof log limits 2026-05-30 14:23:40 +02:00
Peter Steinberger
be3153cabb fix(update): guard startup timestamps 2026-05-30 08:18:55 -04:00
Vincent Koc
56995069f1 fix(ci): preserve goal continuation prompts 2026-05-30 13:17:57 +01:00
Vincent Koc
2238e0ce76 fix(e2e): reject loose tool search fetch limits 2026-05-30 14:17:15 +02:00
Vincent Koc
38a463fe93 fix(deps): remove sharp from root package 2026-05-30 13:15:05 +01:00
Vincent Koc
e1f462b352 fix(e2e): reject loose Telegram Bot API limits 2026-05-30 14:11:43 +02:00
Peter Steinberger
ccd635fdb9 fix(memory-core): guard short-term recall timestamps 2026-05-30 08:10:54 -04:00
Vincent Koc
27dce6c6bb refactor: share embedded run abort loop 2026-05-30 14:09:15 +02:00
Peter Steinberger
9c08d8cd35 fix(memory-core): guard injected timestamps 2026-05-30 08:06:42 -04:00
Vincent Koc
dc5b3ecc4c fix(tui): continue goal commands after creation 2026-05-30 13:03:33 +01:00
Ayaan Zaidi
95f66a34e7 fix(gateway): honor queued manual restarts 2026-05-30 17:33:18 +05:30
Ayaan Zaidi
1695ee2f43 fix(gateway): defer recovery restarts to callers 2026-05-30 17:33:18 +05:30
Ayaan Zaidi
801520b0f0 fix(gateway): consume recovery restart edge cases 2026-05-30 17:33:18 +05:30
Ayaan Zaidi
8ba79d72b4 test(gateway): cover reload stop timeout restart 2026-05-30 17:33:18 +05:30
Ayaan Zaidi
5876ba6152 fix(gateway): restart channels after timed-out reload stop 2026-05-30 17:33:18 +05:30
Peter Steinberger
5b895f2592 fix(memory-wiki): guard injected timestamps 2026-05-30 08:02:26 -04:00
Peter Steinberger
fb61363763 fix(auto-reply): guard date stamp formatting 2026-05-30 07:58:51 -04:00
Vincent Koc
07e0af44b3 fix(e2e): reject loose MCP channel limits 2026-05-30 13:55:39 +02:00
Peter Steinberger
059d5405fe fix(infra): guard backup creation timestamps 2026-05-30 07:53:55 -04:00
Vincent Koc
cd37dbd4e5 refactor: share block reply coalescer enqueue 2026-05-30 13:51:47 +02:00
Vincent Koc
3e8d06a6be fix(ci): include workflow guard target 2026-05-30 12:50:38 +01:00
Peter Steinberger
2f07e4e6c0 fix(agents): guard current time context timestamp 2026-05-30 07:47:11 -04:00
Peter Steinberger
15fb3314de fix(discord): guard model picker legacy dates 2026-05-30 07:43:47 -04:00
Peter Steinberger
5a019e7725 fix(auto-reply): guard subagent info timestamps 2026-05-30 07:34:01 -04:00
Vincent Koc
aea31934d4 refactor: share directory id collection 2026-05-30 13:32:27 +02:00
Peter Steinberger
8ec7e80cb2 fix(agents): bound cli oauth jwt expiries 2026-05-30 07:29:59 -04:00
Peter Steinberger
6c3533d8c4 fix(ui): guard debug event timestamps 2026-05-30 07:23:02 -04:00
Vincent Koc
9c313a7826 fix(test): preserve live test passthrough flags 2026-05-30 13:20:02 +02:00
Peter Steinberger
368a719879 fix(ui): guard dreaming next-cycle timestamps 2026-05-30 07:19:22 -04:00
Peter Steinberger
ec7e3eaf64 fix(ui): guard chat picker session timestamps 2026-05-30 07:15:40 -04:00
Vincent Koc
8bcdab8933 refactor: share oauth identity safety check 2026-05-30 13:14:10 +02:00
Peter Steinberger
c2f0d811e7 fix(ui): guard next run weekday formatting 2026-05-30 07:12:51 -04:00
Peter Steinberger
8f3d3a549d fix(ui): guard usage chart timestamps 2026-05-30 07:10:21 -04:00
Peter Steinberger
d389a52494 fix(ui): centralize invalid date formatting 2026-05-30 07:07:13 -04:00
Vincent Koc
346b14a51a fix(test): route conventional script tests 2026-05-30 13:00:33 +02:00
Vincent Koc
ffa2da8478 fix(test): skip broad changed import scans 2026-05-30 13:00:33 +02:00
Vincent Koc
61a768be75 fix(test): route script library changes 2026-05-30 13:00:33 +02:00
Vincent Koc
3d8a77a113 fix(test): route package tooling changes 2026-05-30 13:00:33 +02:00
Vincent Koc
a6a358f1a6 fix(test): route ci tooling changes 2026-05-30 13:00:33 +02:00
Vincent Koc
131dc4eaeb fix(test): route workflow helper changes 2026-05-30 13:00:33 +02:00
Vincent Koc
022fd55bad fix(test): route crabbox changed tests 2026-05-30 13:00:33 +02:00
Vincent Koc
d9820e4098 fix(ci): disable crabbox on-demand fallback 2026-05-30 13:00:33 +02:00
Vincent Koc
a4ebdc9aa1 fix(test): guard run-with-env help 2026-05-30 13:00:32 +02:00
Vincent Koc
cf2461f7f6 fix(test): guard live runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
f5f829db79 fix(test): guard tsdown runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
a06daab97e fix(test): guard build runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
09f094057a fix(test): guard verify runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
9def042fab fix(test): guard check runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
f6adea5757 fix(test): guard force runner help 2026-05-30 13:00:32 +02:00
Vincent Koc
78f4a5c05f fix(tooling): ignore inline type-only re-exports 2026-05-30 13:00:32 +02:00
Vincent Koc
731a7af9c5 fix(test): keep wrapper help metadata-only 2026-05-30 13:00:32 +02:00
Vincent Koc
ffa4342a6a fix(test): route docker e2e script targets 2026-05-30 13:00:32 +02:00
Vincent Koc
550a134cf9 fix(tooling): forward oxlint shard cancellation 2026-05-30 13:00:32 +02:00
Vincent Koc
1b43e84d0d fix(test): batch explicit source route resolution 2026-05-30 13:00:32 +02:00
Vincent Koc
31f0635f4f fix(test): route explicit source targets narrowly 2026-05-30 13:00:31 +02:00
Vincent Koc
1c65e2e7c1 fix(tooling): bound oxlint shard stalls 2026-05-30 13:00:31 +02:00
Vincent Koc
b6f3fe7938 fix(test): route explicit helper targets narrowly 2026-05-30 13:00:31 +02:00
Vincent Koc
d65b3a68aa perf(cli): keep plugins JSON list on snapshot path 2026-05-30 13:00:31 +02:00
Vincent Koc
e2b54fecd8 fix(doctor): reuse lazy state migration import 2026-05-30 13:00:31 +02:00
Vincent Koc
b8067d073a fix(extensions): keep subagent hook facades lazy 2026-05-30 13:00:31 +02:00
Vincent Koc
e420c001d0 perf(policy): cache doctor file reads 2026-05-30 13:00:31 +02:00
Vincent Koc
44b6b79a66 perf(plugin-sdk): cache runtime helper imports 2026-05-30 13:00:31 +02:00
Vincent Koc
3ef2935ac9 perf(browser): reuse chrome mcp import 2026-05-30 13:00:31 +02:00
Vincent Koc
fced29de17 perf(extensions): cache meeting runtime loaders 2026-05-30 13:00:31 +02:00
Vincent Koc
4f074c3235 perf(extensions): cache plugin runtime loaders 2026-05-30 13:00:31 +02:00
Vincent Koc
5df00520cb perf(extensions): cache provider runtime imports 2026-05-30 13:00:30 +02:00
Vincent Koc
b2c85bc0a2 perf(browser): cache registration runtime import 2026-05-30 13:00:30 +02:00
Vincent Koc
5e2e78a75a perf(wizard): cache setup migration imports 2026-05-30 13:00:30 +02:00
Vincent Koc
2196f107da perf(gateway): cache post-attach startup imports 2026-05-30 13:00:30 +02:00
Vincent Koc
ff56a2d7b3 perf(gateway): cache plugin bootstrap imports 2026-05-30 13:00:30 +02:00
Vincent Koc
24cff8a3bc perf(gateway): share model catalog module loader 2026-05-30 13:00:30 +02:00
Vincent Koc
b495ac2abb perf(gateway): cache remote skills startup import 2026-05-30 13:00:30 +02:00
Vincent Koc
3f2585424d perf(gateway): cache plugin HTTP imports 2026-05-30 13:00:30 +02:00
Vincent Koc
9d1a3007d9 perf(gateway): cache model catalog imports 2026-05-30 13:00:30 +02:00
Vincent Koc
b5c163dffa test(doctor): complete browser health mock 2026-05-30 13:00:30 +02:00
Vincent Koc
ee0cf9e5bb perf(gateway): cache session event imports 2026-05-30 13:00:30 +02:00
Vincent Koc
37fdfa0e0b perf(doctor): cache health contribution imports 2026-05-30 13:00:30 +02:00
Vincent Koc
d550b804b8 perf(doctor): cache core check imports 2026-05-30 13:00:30 +02:00
Vincent Koc
05988500bc perf(crestodian): cache operation imports 2026-05-30 13:00:29 +02:00
Vincent Koc
b01290cf64 perf(cli): cache command ownership imports 2026-05-30 13:00:29 +02:00
Vincent Koc
117f6fb254 test(agents): complete provider runtime mock 2026-05-30 13:00:29 +02:00
Vincent Koc
c363816fea perf(cli): cache runtime startup imports 2026-05-30 13:00:29 +02:00
Vincent Koc
aeed31cdb1 perf(cli): cache root help imports 2026-05-30 13:00:29 +02:00
Vincent Koc
58c8c022c5 perf(entry): cache root help module imports 2026-05-30 13:00:29 +02:00
Vincent Koc
2cfae61743 perf(onboarding): split ClawHub install error codes 2026-05-30 13:00:29 +02:00
Vincent Koc
c6b4daf426 perf(health): remove duplicate config import 2026-05-30 13:00:29 +02:00
Vincent Koc
348fabe04d perf(auto-reply): remove reset model duplicate import 2026-05-30 13:00:29 +02:00
Vincent Koc
6c83e8e7e4 perf(models): cache provider index catalog import 2026-05-30 13:00:29 +02:00
Vincent Koc
817b6259c4 perf(agents): cache live model runtime import 2026-05-30 13:00:29 +02:00
Vincent Koc
959af0fa5b perf(cli): cache secrets command imports 2026-05-30 13:00:29 +02:00
Vincent Koc
669b26a3dc perf(cli): cache routed command imports 2026-05-30 13:00:28 +02:00
Vincent Koc
67c139fc36 perf(cli): cache status command imports 2026-05-30 13:00:28 +02:00
Vincent Koc
8b6829e1bc perf(cli): cache plugin runtime imports 2026-05-30 13:00:28 +02:00
Vincent Koc
86e6fbcf52 perf(cli): cache agent bind command import 2026-05-30 13:00:28 +02:00
Vincent Koc
9b4b3aa348 perf(cli): cache plugins command imports 2026-05-30 13:00:28 +02:00
Vincent Koc
51ab2c0d79 perf(cli): cache models runtime import 2026-05-30 13:00:28 +02:00
Vincent Koc
bdd9c70787 perf(cli): cache devices runtime import 2026-05-30 13:00:28 +02:00
Vincent Koc
1ff95ff3e6 perf(doctor): cache health config import 2026-05-30 13:00:28 +02:00
Peter Steinberger
7c5b55c5ff fix(ui): ignore invalid reset timestamps 2026-05-30 07:00:01 -04:00
Vincent Koc
b0d6076208 refactor: share setup dashboard open flow 2026-05-30 12:55:19 +02:00
Peter Steinberger
4385e57dce fix(doctor): tolerate invalid cron atMs 2026-05-30 06:54:58 -04:00
Vincent Koc
eb45c1c623 fix(scripts): report missing workflow linter fallback 2026-05-30 12:52:54 +02:00
Peter Steinberger
adf981de89 fix(imessage): tolerate invalid catchup cursor timestamps 2026-05-30 06:46:09 -04:00
Peter Steinberger
023a101b91 fix(heartbeat): tolerate invalid commitment due timestamps 2026-05-30 06:41:16 -04:00
Peter Steinberger
8b92aca27f refactor: extract media understanding common package (#88297)
* refactor: extract media understanding common package

* test: move media understanding format test
2026-05-30 12:40:49 +02:00
Peter Steinberger
b13fb788b5 fix(commitments): tolerate invalid due timestamps 2026-05-30 06:36:49 -04:00
Vincent Koc
87c0ee7685 refactor: share config observe recovery restore helpers 2026-05-30 12:35:36 +02:00
Peter Steinberger
eef32e94c7 fix(memory-wiki): tolerate invalid source mtimes 2026-05-30 06:33:13 -04:00
Peter Steinberger
1350efcfd8 fix(acp): tolerate invalid status timestamps 2026-05-30 06:27:44 -04:00
Peter Steinberger
e7ef051149 fix(slack): tolerate invalid interaction datetimes 2026-05-30 06:23:39 -04:00
Peter Steinberger
2b5ddf8f2a fix(acp): tolerate invalid session timestamps 2026-05-30 06:19:44 -04:00
Vincent Koc
6f655573d3 refactor: share parallels smoke lifecycle 2026-05-30 12:18:46 +02:00
Peter Steinberger
8aabf45ddb fix(memory-wiki): tolerate invalid chatgpt timestamps 2026-05-30 06:16:03 -04:00
Peter Steinberger
4d4748e807 fix(voice-call): tolerate invalid ended timestamps 2026-05-30 06:10:40 -04:00
Peter Steinberger
439c09668e fix(ui): ignore invalid usage export timestamps 2026-05-30 06:06:19 -04:00
Peter Steinberger
54bbe87cd5 fix(ui): ignore invalid chat export timestamps 2026-05-30 06:02:38 -04:00
Peter Steinberger
6804b7cb71 fix(matrix): ignore invalid device timestamps 2026-05-30 05:59:10 -04:00
Peter Steinberger
63470e99f0 fix(session): tolerate invalid lifecycle expiry 2026-05-30 05:53:24 -04:00
342 changed files with 10269 additions and 4571 deletions

View File

@@ -6,18 +6,10 @@ class: standard
capacity:
market: spot
strategy: most-available
fallback: on-demand-after-120s
# Fail closed instead of silently falling back to on-demand while the
# Azure-backed billing account is the default runner path.
fallback: spot-only
hints: true
availabilityZones:
- eu-west-1a
- eu-west-1b
- eu-west-1c
regions:
- eu-west-1
- eu-west-2
- eu-central-1
- us-east-1
- us-west-2
actions:
workflow: .github/workflows/crabbox-hydrate.yml
# Default AWS hydration uses local Actions replay. Use
@@ -37,6 +29,8 @@ blacksmith:
job: check
ref: main
aws:
# AWS-specific overrides still pin direct `--provider aws` runs without
# leaking AWS region names into the Azure default capacity fallback list.
region: eu-west-1
rootGB: 400
sync:

View File

@@ -1420,10 +1420,12 @@ jobs:
find src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
find packages/llm-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
touch -t 200001010000 \
if [ -d packages/llm-core/src ]; then
find packages/llm-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
cache_inputs=(
tsconfig.json \
tsconfig.plugin-sdk.dts.json \
packages/plugin-sdk/tsconfig.json \
@@ -1435,6 +1437,12 @@ jobs:
scripts/lib/plugin-sdk-entries.mjs \
package.json \
pnpm-lock.yaml
)
for cache_input in "${cache_inputs[@]}"; do
if [ -e "$cache_input" ]; then
touch -t 200001010000 "$cache_input"
fi
done
- name: Run additional check shard
env:

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
@@ -33,6 +34,16 @@ Docs: https://docs.openclaw.ai
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
- 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: 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.
- CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.
- CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.
- CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.
- CI/tooling: route script edits through conventional owner tests when matching `test/scripts` or `src/scripts` coverage already exists.
- Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, and single-entry store writes.
## 2026.5.28

View File

@@ -17,6 +17,13 @@ export {
import { buildAnthropicVertexProvider } from "./provider-catalog.js";
import { hasAnthropicVertexAvailableAuth } from "./region.js";
let streamRuntimeModulePromise: Promise<typeof import("./stream-runtime.js")> | null = null;
const loadStreamRuntimeModule = async () => {
streamRuntimeModulePromise ??= import("./stream-runtime.js");
return await streamRuntimeModulePromise;
};
export function mergeImplicitAnthropicVertexProvider(params: {
existing?: ReturnType<typeof buildAnthropicVertexProvider>;
implicit: ReturnType<typeof buildAnthropicVertexProvider>;
@@ -50,7 +57,7 @@ export function createAnthropicVertexStreamFn(
baseURL?: string,
deps?: AnthropicVertexStreamDeps,
): StreamFn {
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
runtime.createAnthropicVertexStreamFn(projectId, region, baseURL, deps),
);
return async (model, context, options) => {
@@ -64,7 +71,7 @@ export function createAnthropicVertexStreamFnForModel(
env: NodeJS.ProcessEnv = process.env,
deps?: AnthropicVertexStreamDeps,
): StreamFn {
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
runtime.createAnthropicVertexStreamFnForModel(model, env, deps),
);
return async (...args) => {

View File

@@ -15,6 +15,15 @@ import { BrowserToolSchema } from "./src/browser-tool.schema.js";
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
let browserRegistrationRuntimeModulePromise: Promise<
typeof import("./register.runtime.js")
> | null = null;
const loadBrowserRegistrationRuntimeModule = async () => {
browserRegistrationRuntimeModulePromise ??= import("./register.runtime.js");
return await browserRegistrationRuntimeModulePromise;
};
function isTruthyEnvValue(value: string | undefined): boolean {
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
}
@@ -51,7 +60,7 @@ function createLazyBrowserTool(opts?: {
].join(" "),
parameters: BrowserToolSchema,
execute: async (toolCallId, args, signal, onUpdate) => {
const { createBrowserTool } = await import("./register.runtime.js");
const { createBrowserTool } = await loadBrowserRegistrationRuntimeModule();
const tool = createBrowserTool(opts);
return await tool.execute(toolCallId, args, signal, onUpdate);
},
@@ -65,7 +74,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
command: "browser.proxy",
cap: "browser",
handle: async (paramsJSON) => {
const { runBrowserProxyCommand } = await import("./register.runtime.js");
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
return await runBrowserProxyCommand(paramsJSON);
},
},
@@ -73,7 +82,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
export const browserSecurityAuditCollectors: OpenClawPluginSecurityAuditCollector[] = [
async (ctx) => {
const { collectBrowserSecurityAuditFindings } = await import("./register.runtime.js");
const { collectBrowserSecurityAuditFindings } = await loadBrowserRegistrationRuntimeModule();
return collectBrowserSecurityAuditFindings(ctx);
},
];
@@ -82,7 +91,7 @@ function createLazyBrowserPluginService(): OpenClawPluginService {
let service: OpenClawPluginService | null = null;
const loadService = async () => {
if (!service) {
const { createBrowserPluginService } = await import("./register.runtime.js");
const { createBrowserPluginService } = await loadBrowserRegistrationRuntimeModule();
service = createBrowserPluginService();
}
return service;
@@ -124,7 +133,7 @@ export function registerBrowserPlugin(api: OpenClawPluginApi) {
api.registerGatewayMethod(
BROWSER_REQUEST_GATEWAY_METHOD,
async (opts) => {
const { handleBrowserGatewayRequest } = await import("./register.runtime.js");
const { handleBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
return await handleBrowserGatewayRequest(opts);
},
{

View File

@@ -1,6 +1,6 @@
import { redactCdpUrl } from "../cdp.helpers.js";
import { snapshotAria } from "../cdp.js";
import { getChromeMcpPid } from "../chrome-mcp.js";
import { getChromeMcpPid, takeChromeMcpSnapshot } from "../chrome-mcp.js";
import { resolveBrowserExecutableForPlatform } from "../chrome.executables.js";
import { resolveManagedBrowserHeadlessMode } from "../config.js";
import { buildBrowserDoctorReport } from "../doctor.js";
@@ -227,7 +227,6 @@ async function runBrowserLiveProbe(req: BrowserRequest, ctx: BrowserRouteContext
try {
const tab = await profileCtx.ensureTabAvailable();
if (capabilities.usesChromeMcp) {
const { takeChromeMcpSnapshot } = await import("../chrome-mcp.js");
await takeChromeMcpSnapshot({
profileName: profileCtx.profile.name,
profile: profileCtx.profile,

View File

@@ -13,13 +13,20 @@ export type CodexAppServerClientFactory = (
config?: AuthProfileOrderConfig,
) => Promise<CodexAppServerClient>;
let sharedClientModulePromise: Promise<typeof import("./shared-client.js")> | null = null;
const loadSharedClientModule = async () => {
sharedClientModulePromise ??= import("./shared-client.js");
return await sharedClientModulePromise;
};
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
startOptions,
authProfileId,
agentDir,
config,
) =>
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
loadSharedClientModule().then(({ getSharedCodexAppServerClient }) =>
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
);
@@ -29,6 +36,6 @@ export const defaultLeasedCodexAppServerClientFactory: CodexAppServerClientFacto
agentDir,
config,
) =>
import("./shared-client.js").then(({ getLeasedSharedCodexAppServerClient }) =>
loadSharedClientModule().then(({ getLeasedSharedCodexAppServerClient }) =>
getLeasedSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
);

View File

@@ -54,6 +54,21 @@ describe("DiffArtifactStore", () => {
expect(await store.readHtml(artifact.id)).toBe("<html>demo</html>");
});
it("caps artifact expiry instead of throwing near the Date boundary", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
const artifact = await store.createArtifact({
html: "<html>demo</html>",
title: "Demo",
inputKind: "patch",
fileCount: 1,
ttlMs: 60_000,
});
expect(artifact.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
});
it("expires artifacts after the ttl", async () => {
vi.useFakeTimers();
const now = new Date("2026-02-27T16:00:00Z");
@@ -131,6 +146,15 @@ describe("DiffArtifactStore", () => {
});
});
it("caps standalone file expiry instead of throwing near the Date boundary", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
const standalone = await store.createStandaloneFileArtifact({ ttlMs: 60_000 });
expect(standalone.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
});
it("expires standalone file artifacts using ttl metadata", async () => {
vi.useFakeTimers();
const now = new Date("2026-02-27T16:00:00Z");

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { PluginLogger } from "../api.js";
@@ -64,15 +65,16 @@ export class DiffArtifactStore {
const htmlPath = path.join(artifactDir, "viewer.html");
const ttlMs = normalizeTtlMs(params.ttlMs);
const createdAt = new Date();
const expiresAt = new Date(createdAt.getTime() + ttlMs);
const createdAtIso = createdAt.toISOString();
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
const meta: DiffArtifactMeta = {
id,
token,
title: params.title,
inputKind: params.inputKind,
fileCount: params.fileCount,
createdAt: createdAt.toISOString(),
expiresAt: expiresAt.toISOString(),
createdAt: createdAtIso,
expiresAt,
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
htmlPath,
...(params.context ? { context: params.context } : {}),
@@ -144,11 +146,12 @@ export class DiffArtifactStore {
const filePath = path.join(artifactDir, `preview.${format}`);
const ttlMs = normalizeTtlMs(params.ttlMs);
const createdAt = new Date();
const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
const createdAtIso = createdAt.toISOString();
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
const meta: StandaloneFileMeta = {
kind: "standalone_file",
id,
createdAt: createdAt.toISOString(),
createdAt: createdAtIso,
expiresAt,
filePath: this.normalizeStoredPath(filePath, "filePath"),
...(params.context ? { context: params.context } : {}),
@@ -357,6 +360,14 @@ function normalizeTtlMs(value?: number): number {
return Math.min(rounded, MAX_TTL_MS);
}
function resolveExpiresAtIso(createdAtMs: number, ttlMs: number): string {
return (
timestampMsToIsoString(createdAtMs + ttlMs) ??
timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ??
"1970-01-01T00:00:00.000Z"
);
}
function isExpired(meta: { expiresAt: string }): boolean {
const expiresAt = Date.parse(meta.expiresAt);
if (!Number.isFinite(expiresAt)) {

View File

@@ -63,4 +63,88 @@ describe("Discord model picker preference migration", () => {
updatedAt: "2026-05-29T00:00:00.001Z",
});
});
it("plans legacy JSON import with max Date timestamps", async () => {
const stateDir = await makeStateDir();
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(
sourcePath,
JSON.stringify({
version: 1,
entries: {
"discord:default:dm:user:max-date": {
recent: ["openai/gpt-5", "openai/gpt-4.1"],
updatedAt: "+275760-09-13T00:00:00.000Z",
},
},
}),
);
const plans = await Promise.resolve(
detectDiscordLegacyStateMigrations({
cfg: {},
env: {},
oauthDir: path.join(stateDir, "credentials"),
stateDir,
}),
);
const plan = plans?.[0];
if (plan?.kind !== "plugin-state-import") {
throw new Error("expected plugin-state import plan");
}
const entries = await plan.readEntries();
expect(
entries.map((entry) => {
const value = entry.value as { updatedAt?: unknown };
return value.updatedAt;
}),
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
});
it("keeps legacy JSON import order near max Date", async () => {
const stateDir = await makeStateDir();
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(
sourcePath,
JSON.stringify({
version: 1,
entries: {
"discord:default:dm:user:near-max-date": {
recent: ["openai/gpt-5", "openai/gpt-4.1"],
updatedAt: "+275760-09-12T23:59:59.999Z",
},
},
}),
);
const plans = await Promise.resolve(
detectDiscordLegacyStateMigrations({
cfg: {},
env: {},
oauthDir: path.join(stateDir, "credentials"),
stateDir,
}),
);
const plan = plans?.[0];
if (plan?.kind !== "plugin-state-import") {
throw new Error("expected plugin-state import plan");
}
const entries = await plan.readEntries();
expect(
entries.map((entry) => {
const value = entry.value as { modelRef?: unknown };
return value.modelRef;
}),
).toEqual(["openai/gpt-5", "openai/gpt-4.1"]);
expect(
entries.map((entry) => {
const value = entry.value as { updatedAt?: unknown };
return value.updatedAt;
}),
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
});
});

View File

@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type { BundledChannelLegacyStateMigrationDetector } from "openclaw/plugin-sdk/channel-entry-contract";
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
const PREFERENCE_MAX_ENTRIES = 2_000;
@@ -92,7 +93,15 @@ function timestampMs(value: unknown): number {
}
function legacyUpdatedAtForIndex(updatedAt: unknown, index: number, total: number): string {
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
const baseMs = timestampMs(updatedAt);
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
const shiftedMs = anchorMs - Math.max(0, index);
return (
timestampMsToIsoString(shiftedMs) ??
timestampMsToIsoString(baseMs) ??
timestampMsToIsoString(Math.max(0, total - index)) ??
"1970-01-01T00:00:00.000Z"
);
}
export const detectDiscordLegacyStateMigrations: BundledChannelLegacyStateMigrationDetector = ({

View File

@@ -6,7 +6,7 @@ import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { setDiscordRuntime, type DiscordRuntime } from "../runtime.js";
import {
buildDiscordModelPickerPreferenceKey,
@@ -163,6 +163,68 @@ describe("discord model picker preferences", () => {
]);
});
it("imports legacy JSON preferences with max Date timestamps", async () => {
const env = await createStateEnv();
const scope = { accountId: "main", guildId: "guild-max-date", userId: "user-max-date" };
const key = buildDiscordModelPickerPreferenceKey(scope);
expect(key).toBeTruthy();
const legacyPath = path.join(
env.OPENCLAW_STATE_DIR as string,
"discord",
"model-picker-preferences.json",
);
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
await fs.writeFile(
legacyPath,
JSON.stringify({
version: 1,
entries: {
[key as string]: {
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
updatedAt: "+275760-09-13T00:00:00.000Z",
},
},
}),
"utf8",
);
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
"openai/gpt-4.1",
"openai/gpt-4o",
]);
});
it("preserves legacy JSON preference order near max Date", async () => {
const env = await createStateEnv();
const scope = { accountId: "main", guildId: "guild-near-max-date", userId: "user-near-max" };
const key = buildDiscordModelPickerPreferenceKey(scope);
expect(key).toBeTruthy();
const legacyPath = path.join(
env.OPENCLAW_STATE_DIR as string,
"discord",
"model-picker-preferences.json",
);
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
await fs.writeFile(
legacyPath,
JSON.stringify({
version: 1,
entries: {
[key as string]: {
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
updatedAt: "+275760-09-12T23:59:59.999Z",
},
},
}),
"utf8",
);
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
"openai/gpt-4.1",
"openai/gpt-4o",
]);
});
it("skips malformed legacy JSON entries during import", async () => {
const env = await createStateEnv();
const scope = { userId: "valid-legacy-user" };
@@ -207,4 +269,34 @@ describe("discord model picker preferences", () => {
const recent = await readDiscordModelPickerRecentModels({ env, scope });
expect(new Set(recent)).toEqual(new Set(["openai/gpt-4o", "openai/gpt-4.1"]));
});
it("keeps selections recent when the process clock is outside the Date range", async () => {
const env = await createStateEnv();
const scope = { userId: "invalid-clock-user" };
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4.1" });
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
try {
await recordDiscordModelPickerRecentModel({
env,
scope,
modelRef: "openai/gpt-5.5",
limit: 2,
});
await recordDiscordModelPickerRecentModel({
env,
scope,
modelRef: "openai/gpt-5.6",
limit: 2,
});
} finally {
dateNowSpy.mockRestore();
}
await expect(readDiscordModelPickerRecentModels({ env, scope, limit: 3 })).resolves.toEqual([
"openai/gpt-5.6",
"openai/gpt-5.5",
]);
});
});

View File

@@ -3,6 +3,12 @@ import os from "node:os";
import path from "node:path";
import { normalizeAccountId as normalizeSharedAccountId } from "openclaw/plugin-sdk/account-id";
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
import {
MAX_DATE_TIMESTAMP_MS,
resolveDateTimestampMs,
resolveTimestampMsToIsoString,
timestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -13,11 +19,13 @@ const PREFERENCE_MAX_ENTRIES = 2_000;
const MAX_PLUGIN_STATE_KEY_BYTES = 512;
const textEncoder = new TextEncoder();
let lastPreferenceTimestampMs = 0;
let lastPreferenceOrder = 0;
type ModelPickerPreferencesEntry = {
scopeKey: string;
modelRef: string;
updatedAt: string;
updatedOrder?: number;
};
type LegacyModelPickerPreferencesEntry = {
@@ -124,6 +132,7 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
scopeKey?: unknown;
modelRef?: unknown;
updatedAt?: unknown;
updatedOrder?: unknown;
};
if (typeof typedValue.scopeKey !== "string" || typeof typedValue.modelRef !== "string") {
return undefined;
@@ -136,6 +145,10 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
scopeKey: typedValue.scopeKey,
modelRef,
updatedAt: typeof typedValue.updatedAt === "string" ? typedValue.updatedAt : "",
updatedOrder:
typeof typedValue.updatedOrder === "number" && Number.isSafeInteger(typedValue.updatedOrder)
? typedValue.updatedOrder
: undefined,
};
}
@@ -152,13 +165,58 @@ function timestampMs(value: string): number {
return Number.isFinite(parsed) ? parsed : 0;
}
function legacyUpdatedAtForIndex(updatedAt: string, index: number, total: number): string {
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
function timestampOrder(value?: number): number {
return value !== undefined && value >= 0 ? value : 0;
}
function nextPreferenceTimestampIso(): string {
lastPreferenceTimestampMs = Math.max(Date.now(), lastPreferenceTimestampMs + 1);
return new Date(lastPreferenceTimestampMs).toISOString();
function comparePreferenceEntries(
left: { key: string; value: ModelPickerPreferencesEntry },
right: { key: string; value: ModelPickerPreferencesEntry },
): number {
return (
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) ||
left.key.localeCompare(right.key)
);
}
function legacyUpdatedAtForIndex(updatedAt: string, index: number, total: number): string {
const baseMs = timestampMs(updatedAt);
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
const shiftedMs = anchorMs - Math.max(0, index);
return (
timestampMsToIsoString(shiftedMs) ??
timestampMsToIsoString(baseMs) ??
timestampMsToIsoString(Math.max(0, total - index)) ??
"1970-01-01T00:00:00.000Z"
);
}
function nextPreferenceTimestamp(existingEntries: ModelPickerPreferencesEntry[]): {
updatedAt: string;
updatedOrder: number;
} {
const existingMaxTimestampMs = existingEntries.reduce(
(max, entry) => Math.max(max, timestampMs(entry.updatedAt)),
0,
);
lastPreferenceTimestampMs = Math.min(
Math.max(
resolveDateTimestampMs(Date.now(), 0),
lastPreferenceTimestampMs + 1,
existingMaxTimestampMs + 1,
),
MAX_DATE_TIMESTAMP_MS,
);
const existingMaxOrder = existingEntries.reduce(
(max, entry) => Math.max(max, timestampOrder(entry.updatedOrder)),
0,
);
lastPreferenceOrder = Math.max(lastPreferenceOrder + 1, existingMaxOrder + 1);
return {
updatedAt: resolveTimestampMsToIsoString(lastPreferenceTimestampMs),
updatedOrder: lastPreferenceOrder,
};
}
function normalizeLegacyPreferenceKey(key: string): string | undefined {
@@ -237,10 +295,13 @@ export async function readDiscordModelPickerRecentModels(params: {
await importLegacyPreferences(params.env);
const store = openPreferenceStore(params.env);
const recent = (await store.entries())
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key)
.toSorted((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt))
.map((entry) => entry.modelRef);
.map((entry) => ({ key: entry.key, value: sanitizeStoredPreferenceEntry(entry.value) }))
.filter(
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
entry.value?.scopeKey === key,
)
.toSorted(comparePreferenceEntries)
.map((entry) => entry.value.modelRef);
if (!params.allowedModelRefs || params.allowedModelRefs.size === 0) {
return sanitizeRecentModels(recent, limit);
}
@@ -268,10 +329,14 @@ export async function recordDiscordModelPickerRecentModel(params: {
try {
await importLegacyPreferences(params.env);
const store = openPreferenceStore(params.env);
const existingEntries = (await store.entries())
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key);
const timestamp = nextPreferenceTimestamp(existingEntries);
await store.register(buildPreferenceModelKey(key, normalizedModelRef), {
scopeKey: key,
modelRef: normalizedModelRef,
updatedAt: nextPreferenceTimestampIso(),
...timestamp,
});
const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
const scopedEntries = (await store.entries())
@@ -280,11 +345,7 @@ export async function recordDiscordModelPickerRecentModel(params: {
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
entry.value?.scopeKey === key,
)
.toSorted(
(left, right) =>
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
left.key.localeCompare(right.key),
);
.toSorted(comparePreferenceEntries);
await Promise.all(scopedEntries.slice(limit).map((entry) => store.delete(entry.key)));
} catch {
return;

View File

@@ -121,6 +121,7 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
afterAll(() => {
@@ -322,6 +323,18 @@ describe("sendMessageDiscord", () => {
).toBeTypeOf("string");
});
it("rejects timeout durations outside Date range", async () => {
const { rest, patchMock } = makeDiscordRest();
await expect(
timeoutMemberDiscord(
{ guildId: "g1", userId: "u1", durationMinutes: 8_640_000_000_000_001 },
discordClientOpts(rest),
),
).rejects.toThrow("Discord timeout duration is outside the supported Date range");
expect(patchMock).not.toHaveBeenCalled();
});
it("adds and removes roles", async () => {
const { rest, putMock, deleteMock } = makeDiscordRest();
putMock.mockResolvedValue({});

View File

@@ -6,6 +6,7 @@ import type {
APIVoiceState,
RESTPostAPIGuildScheduledEventJSONBody,
} from "discord-api-types/v10";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
import {
@@ -139,7 +140,10 @@ export async function timeoutMemberDiscord(
let until = payload.until;
if (!until && payload.durationMinutes) {
const ms = payload.durationMinutes * 60 * 1000;
until = new Date(Date.now() + ms).toISOString();
until = timestampMsToIsoString(Date.now() + ms);
if (!until) {
throw new Error("Discord timeout duration is outside the supported Date range");
}
}
return await timeoutGuildMember(rest, payload.guildId, payload.userId, {
body: { communication_disabled_until: until ?? null },

View File

@@ -251,6 +251,13 @@ function applyNewAppSecurityPolicy(
// Scan-to-create flow
// ---------------------------------------------------------------------------
let appRegistrationModulePromise: Promise<typeof import("./app-registration.js")> | null = null;
const loadAppRegistrationModule = async () => {
appRegistrationModulePromise ??= import("./app-registration.js");
return await appRegistrationModulePromise;
};
async function promptFeishuDomain(params: {
prompter: WizardPrompter;
initialValue?: FeishuDomain;
@@ -281,7 +288,7 @@ async function runScanToCreate(
domain: FeishuDomain,
): Promise<AppRegistrationResult | null> {
const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } =
await import("./app-registration.js");
await loadAppRegistrationModule();
try {
await initAppRegistration(domain);
} catch {
@@ -392,7 +399,7 @@ async function runNewAppFlow(params: {
// Fetch openId via API for manual flow.
if (appId && appSecretProbeValue) {
const { getAppOwnerOpenId } = await import("./app-registration.js");
const { getAppOwnerOpenId } = await loadAppRegistrationModule();
scanOpenId = await getAppOwnerOpenId({
appId,
appSecret: appSecretProbeValue,

View File

@@ -23,9 +23,3 @@ export function registerFeishuSubagentHooks(api: OpenClawPluginApi): void {
handleFeishuSubagentEnded(event);
});
}
export {
handleFeishuSubagentDeliveryTarget,
handleFeishuSubagentEnded,
handleFeishuSubagentSpawning,
} from "./src/subagent-hooks.js";

View File

@@ -37,6 +37,19 @@ import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
import { GoogleMeetRuntime } from "./src/runtime.js";
import { isGoogleMeetBrowserManualActionError } from "./src/transports/chrome-create.js";
let googleMeetCreateModulePromise: Promise<typeof import("./src/create.js")> | null = null;
let googleMeetCliModulePromise: Promise<typeof import("./src/cli.js")> | null = null;
const loadGoogleMeetCreateModule = async () => {
googleMeetCreateModulePromise ??= import("./src/create.js");
return await googleMeetCreateModulePromise;
};
const loadGoogleMeetCliModule = async () => {
googleMeetCliModulePromise ??= import("./src/cli.js");
return await googleMeetCliModulePromise;
};
const googleMeetConfigSchema = {
parse(value: unknown) {
return resolveGoogleMeetConfig(value);
@@ -493,7 +506,7 @@ async function createMeetFromParams(params: {
runtime: OpenClawPluginApi["runtime"];
raw: Record<string, unknown>;
}) {
const create = await import("./src/create.js");
const create = await loadGoogleMeetCreateModule();
return create.createMeetFromParams(params);
}
@@ -503,7 +516,7 @@ async function createAndJoinMeetFromParams(params: {
raw: Record<string, unknown>;
ensureRuntime: () => Promise<GoogleMeetRuntime>;
}) {
const create = await import("./src/create.js");
const create = await loadGoogleMeetCreateModule();
return create.createAndJoinMeetFromParams(params);
}
@@ -615,7 +628,7 @@ async function exportGoogleMeetBundleFromParams(
}),
]);
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } =
await import("./src/cli.js");
await loadGoogleMeetCliModule();
const calendarId = normalizeOptionalString(raw.calendarId);
const request = {
...(resolved.meeting ? { meeting: resolved.meeting } : {}),
@@ -1189,7 +1202,7 @@ export default definePluginEntry({
api.registerCli(
async ({ program }) => {
const { registerGoogleMeetCli } = await import("./src/cli.js");
const { registerGoogleMeetCli } = await loadGoogleMeetCliModule();
registerGoogleMeetCli({
program,
config,

View File

@@ -20,6 +20,13 @@ const ENV_VARS = [
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
] as const;
let oauthRuntimeModulePromise: Promise<typeof import("./oauth.runtime.js")> | null = null;
const loadOauthRuntimeModule = async () => {
oauthRuntimeModulePromise ??= import("./oauth.runtime.js");
return await oauthRuntimeModulePromise;
};
async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) {
return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID);
}
@@ -58,7 +65,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
const spin = ctx.prompter.progress("Starting Gemini CLI OAuth…");
try {
const { loginGeminiCliOAuth } = await import("./oauth.runtime.js");
const { loginGeminiCliOAuth } = await loadOauthRuntimeModule();
const result = await loginGeminiCliOAuth({
isRemote: ctx.isRemote,
openUrl: ctx.openUrl,
@@ -122,7 +129,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
formatApiKey: (cred) => formatGoogleOauthApiKey(cred),
refreshOAuth: async (cred) => {
const { refreshGeminiCliOAuthToken } = await import("./oauth.runtime.js");
const { refreshGeminiCliOAuthToken } = await loadOauthRuntimeModule();
return await refreshGeminiCliOAuthToken(cred);
},
resolveUsageAuth: async (ctx) => {

View File

@@ -100,6 +100,7 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
for (const key of ENV_KEYS) {
const value = envSnapshot[key];
if (value === undefined) {
@@ -452,6 +453,20 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
]);
});
it("rejects browser session expiry outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
const provider = buildGoogleRealtimeVoiceProvider();
await expect(
provider.createBrowserSession?.({
providerConfig: {
apiKey: "gemini-key",
},
}),
).rejects.toThrow("Google realtime browser session expiry is outside the supported Date range");
expect(createTokenMock).not.toHaveBeenCalled();
});
it("can opt out of Google Live session resumption and context compression", async () => {
const provider = buildGoogleRealtimeVoiceProvider();
const bridge = provider.createBridge({

View File

@@ -16,6 +16,7 @@ import type {
ThinkingConfig,
TurnCoverage,
} from "@google/genai";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
import type {
RealtimeVoiceAudioFormat,
@@ -857,6 +858,11 @@ async function createGoogleRealtimeBrowserSession(
const voice = req.voice ?? config.voice ?? GOOGLE_REALTIME_DEFAULT_VOICE;
const expiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_SESSION_TTL_MS;
const newSessionExpiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_NEW_SESSION_TTL_MS;
const expireTime = timestampMsToIsoString(expiresAtMs);
const newSessionExpireTime = timestampMsToIsoString(newSessionExpiresAtMs);
if (!expireTime || !newSessionExpireTime) {
throw new Error("Google realtime browser session expiry is outside the supported Date range");
}
const ai = createGoogleGenAI({
apiKey,
httpOptions: {
@@ -866,8 +872,8 @@ async function createGoogleRealtimeBrowserSession(
const token = await ai.authTokens.create({
config: {
uses: 1,
expireTime: new Date(expiresAtMs).toISOString(),
newSessionExpireTime: new Date(newSessionExpiresAtMs).toISOString(),
expireTime,
newSessionExpireTime,
liveConnectConstraints: {
model,
config: buildGoogleLiveConnectConfig({

View File

@@ -24,7 +24,7 @@ import {
rememberIMessageReplyCache,
type IMessageChatContext,
} from "./monitor-reply-cache.js";
import { getCachedIMessagePrivateApiStatus } from "./probe.js";
import { getCachedIMessagePrivateApiStatus, probeIMessagePrivateApi } from "./probe.js";
import { parseIMessageTarget, type IMessageTarget } from "./targets.js";
const loadIMessageActionsRuntime = createLazyRuntimeNamedExport(
@@ -417,7 +417,6 @@ export const imessageMessageActions: ChannelMessageActionAdapter = {
// status adapter, which doesn't fire eagerly on first dispatch. Run
// an inline probe so the first react/send-rich attempt after `imsg
// launch` succeeds without requiring a manual `channels status`.
const { probeIMessagePrivateApi } = await import("./probe.js");
privateApiStatus = await probeIMessagePrivateApi(
cliPathForProbe,
account.config.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS,

View File

@@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runIMessageCatchup } from "./catchup-bridge.js";
import { resolveCatchupConfig } from "./catchup.js";
import { resolveCatchupConfig, saveIMessageCatchupCursor } from "./catchup.js";
import type { IMessagePayload } from "./types.js";
type RpcCall = {
@@ -157,6 +157,32 @@ describe("runIMessageCatchup", () => {
expect(summary.replayed).toBe(1);
});
it("does not crash on Date-invalid persisted cursor timestamps", async () => {
const log = vi.fn();
await saveIMessageCatchupCursor("default", {
lastSeenMs: 8_700_000_000_000_000,
lastSeenRowid: 10,
});
const { client, calls } = makeFakeClient(() => {
throw new Error("unexpected rpc");
});
const summary = await runIMessageCatchup({
client: client as never,
accountId: "default",
config: resolveCatchupConfig({ enabled: true, perRunLimit: 50, maxAgeMinutes: 60 }),
includeAttachments: false,
dispatchPayload: async () => {},
runtime: { log },
});
expect(summary.querySucceeded).toBe(false);
expect(calls).toEqual([]);
expect(log).toHaveBeenCalledWith(
expect.stringContaining("imessage catchup: invalid since timestamp"),
);
});
it("returns querySucceeded=false when chats.list throws", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-08T12:00:00Z"));

View File

@@ -1,3 +1,4 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { warn } from "openclaw/plugin-sdk/runtime-env";
import type { IMessageRpcClient } from "../client.js";
import {
@@ -88,6 +89,11 @@ export async function runIMessageCatchup(
const payloadByGuid = new Map<string, IMessagePayload>();
const fetchFn: CatchupFetchFn = async ({ sinceMs, sinceRowid, limit }) => {
const sinceISO = timestampMsToIsoString(sinceMs);
if (!sinceISO) {
warnLog(`imessage catchup: invalid since timestamp ${sinceMs}`);
return { resolved: false, rows: [] };
}
let chatsResult: { chats?: ChatsListEntry[] } | undefined;
try {
chatsResult = await client.request<{ chats?: ChatsListEntry[] }>(
@@ -100,7 +106,6 @@ export async function runIMessageCatchup(
return { resolved: false, rows: [] };
}
const chats = chatsResult?.chats ?? [];
const sinceISO = new Date(sinceMs).toISOString();
const collected: IMessageCatchupRow[] = [];
const perChatLimit = Math.min(limit, PER_CHAT_HISTORY_LIMIT_CAP);
let historyFetchFailed = false;

View File

@@ -85,6 +85,12 @@ const loadMatrixChannelRuntime = createLazyRuntimeNamedExport(
() => import("./channel.runtime.js"),
"matrixChannelRuntime",
);
let matrixDoctorModulePromise: Promise<typeof import("./doctor.js")> | null = null;
const loadMatrixDoctorModule = async () => {
matrixDoctorModulePromise ??= import("./doctor.js");
return await matrixDoctorModulePromise;
};
const meta = {
id: "matrix",
@@ -118,9 +124,9 @@ const matrixDoctor: ChannelDoctorAdapter = {
legacyConfigRules: MATRIX_LEGACY_CONFIG_RULES,
normalizeCompatibilityConfig: normalizeMatrixCompatibilityConfig,
runConfigSequence: async ({ cfg, env, shouldRepair }) =>
await (await import("./doctor.js")).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
await (await loadMatrixDoctorModule()).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
cleanStaleConfig: async ({ cfg }) =>
await (await import("./doctor.js")).cleanStaleMatrixPluginConfig(cfg),
await (await loadMatrixDoctorModule()).cleanStaleMatrixPluginConfig(cfg),
};
const listMatrixDirectoryPeersFromConfig =

View File

@@ -950,6 +950,28 @@ describe("matrix CLI verification commands", () => {
expect(console.log).toHaveBeenCalledWith("- BritdXC6iL (OpenClaw Gateway)");
});
it("omits invalid matrix device last seen timestamps", async () => {
listMatrixOwnDevicesMock.mockResolvedValue([
{
deviceId: "DEVICE123",
displayName: "OpenClaw Gateway",
lastSeenIp: "127.0.0.1",
lastSeenTs: 8_700_000_000_000_000,
current: true,
},
]);
const program = buildProgram();
await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" });
expect(console.log).toHaveBeenCalledWith("Account: poe");
expect(console.log).toHaveBeenCalledWith("- DEVICE123 (current, OpenClaw Gateway)");
expect(console.log).toHaveBeenCalledWith(" Last IP: 127.0.0.1");
expect(
consoleLogMock.mock.calls.some(([message]) => String(message).startsWith(" Last seen:")),
).toBe(false);
});
it("prunes stale matrix gateway devices", async () => {
pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({
before: [

View File

@@ -1,6 +1,6 @@
import type { Command } from "commander";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
import { parseStrictInteger, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup";
import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js";
import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js";
@@ -216,8 +216,9 @@ function printMatrixOwnDevices(
console.log(
`- ${formatMatrixCliText(device.deviceId)}${labels.length ? ` (${labels.join(", ")})` : ""}`,
);
if (device.lastSeenTs) {
printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString());
const lastSeenAt = timestampMsToIsoString(device.lastSeenTs);
if (lastSeenAt) {
printTimestamp(" Last seen", lastSeenAt);
}
if (device.lastSeenIp) {
console.log(` Last IP: ${formatMatrixCliText(device.lastSeenIp)}`);

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ensureMatrixStartupVerification } from "./startup-verification.js";
function createTempStateDir(): string {
@@ -80,6 +80,10 @@ function createHarness(params?: {
}
describe("ensureMatrixStartupVerification", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("skips automatic requests when the device is already verified", async () => {
const tempHome = createTempStateDir();
const harness = createHarness({ verified: true });
@@ -203,6 +207,27 @@ describe("ensureMatrixStartupVerification", () => {
expect(fs.existsSync(createStateFilePath(tempHome))).toBe(true);
});
it("falls back when startup verification nowMs is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-05-30T12:00:00.000Z"));
const tempHome = createTempStateDir();
const stateFilePath = createStateFilePath(tempHome);
const harness = createHarness();
const result = await ensureMatrixStartupVerification({
client: harness.client as never,
auth: createAuth(),
accountConfig: {},
stateFilePath,
nowMs: 8_640_000_000_000_001,
});
expect(result.kind).toBe("requested");
const state = JSON.parse(fs.readFileSync(stateFilePath, "utf-8")) as {
attemptedAt?: string;
};
expect(state.attemptedAt).toBe("2026-05-30T12:00:00.000Z");
});
it("keeps startup verification failures non-fatal", async () => {
const tempHome = createTempStateDir();
const harness = createHarness({

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type { MatrixConfig } from "../../types.js";
import { resolveMatrixStoragePaths } from "../client/storage.js";
import type { MatrixAuth } from "../client/types.js";
@@ -95,6 +96,14 @@ function resolveRetryAfterMs(params: {
return remaining > 0 ? remaining : undefined;
}
function resolveStartupVerificationTimestamp(nowMs: unknown): string {
return (
timestampMsToIsoString(nowMs) ??
timestampMsToIsoString(Date.now()) ??
"1970-01-01T00:00:00.000Z"
);
}
function shouldHonorCooldown(params: {
state: MatrixStartupVerificationState | null;
verification: MatrixOwnDeviceVerificationStatus;
@@ -189,6 +198,7 @@ export async function ensureMatrixStartupVerification(params: {
);
const cooldownMs = cooldownHours * 60 * 60 * 1000;
const nowMs = params.nowMs ?? Date.now();
const attemptedAt = resolveStartupVerificationTimestamp(nowMs);
const state = await readStartupVerificationState(statePath);
const stateCooldownMs = resolveStateCooldownMs(state, cooldownMs);
if (shouldHonorCooldown({ state, verification, stateCooldownMs, nowMs })) {
@@ -208,7 +218,7 @@ export async function ensureMatrixStartupVerification(params: {
await writeJsonFileAtomically(statePath, {
userId: verification.userId,
deviceId: verification.deviceId,
attemptedAt: new Date(nowMs).toISOString(),
attemptedAt,
outcome: "requested",
requestId: request.id,
transactionId: request.transactionId,
@@ -224,7 +234,7 @@ export async function ensureMatrixStartupVerification(params: {
await writeJsonFileAtomically(statePath, {
userId: verification.userId,
deviceId: verification.deviceId,
attemptedAt: new Date(nowMs).toISOString(),
attemptedAt,
outcome: "failed",
error,
} satisfies MatrixStartupVerificationState).catch(() => {});

View File

@@ -166,6 +166,26 @@ describe("MatrixVerificationManager", () => {
expect(summary.phaseName).toBe("requested");
});
it("tracks verification requests when the process clock is outside the Date range", () => {
const manager = new MatrixVerificationManager();
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
try {
const summary = manager.trackVerificationRequest(
new MockVerificationRequest({
transactionId: "txn-invalid-clock",
phase: VerificationPhase.Requested,
}),
);
expect(summary.createdAt).toBe("1970-01-01T00:00:00.000Z");
expect(summary.updatedAt).toBe("1970-01-01T00:00:00.000Z");
expect(manager.listVerifications()).toHaveLength(1);
} finally {
dateNowSpy.mockRestore();
}
});
it("reuses the same tracked id for repeated transaction IDs", () => {
const manager = new MatrixVerificationManager();
const first = new MockVerificationRequest({

View File

@@ -4,6 +4,10 @@ import {
VerifierEvent,
} from "matrix-js-sdk/lib/crypto-api/verification.js";
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
import {
resolveDateTimestampMs,
resolveTimestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { formatMatrixErrorMessage } from "../errors.js";
export type MatrixVerificationMethod = "sas" | "show-qr" | "scan-qr";
@@ -266,7 +270,7 @@ export class MatrixVerificationManager {
}
private touchVerificationSession(session: MatrixVerificationSession): void {
session.updatedAtMs = Date.now();
session.updatedAtMs = resolveDateTimestampMs(Date.now());
this.emitVerificationSummary(session);
}
@@ -317,8 +321,8 @@ export class MatrixVerificationManager {
hasReciprocateQr: Boolean(session.reciprocateQrCallbacks),
completed: phase === VerificationPhase.Done,
error: session.error,
createdAt: new Date(session.createdAtMs).toISOString(),
updatedAt: new Date(session.updatedAtMs).toISOString(),
createdAt: resolveTimestampMsToIsoString(session.createdAtMs),
updatedAt: resolveTimestampMsToIsoString(session.updatedAtMs),
};
}
@@ -594,7 +598,7 @@ export class MatrixVerificationManager {
}
}
const now = Date.now();
const now = resolveDateTimestampMs(Date.now());
const id = `verification-${++this.verificationSessionCounter}`;
const session: MatrixVerificationSession = {
id,

View File

@@ -23,9 +23,3 @@ export function registerMatrixSubagentHooks(api: OpenClawPluginApi): void {
return handleMatrixSubagentDeliveryTarget(event);
});
}
export {
handleMatrixSubagentDeliveryTarget,
handleMatrixSubagentEnded,
handleMatrixSubagentSpawning,
} from "./src/matrix/subagent-hooks.js";

View File

@@ -1,11 +1,15 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { writeDailyDreamingPhaseBlock, writeDeepDreamingReport } from "./dreaming-markdown.js";
import { createMemoryCoreTestHarness } from "./test-helpers.js";
const { createTempWorkspace } = createMemoryCoreTestHarness();
afterEach(() => {
vi.restoreAllMocks();
});
async function expectPathMissing(targetPath: string): Promise<void> {
const error = await fs.access(targetPath).then(
() => undefined,
@@ -55,6 +59,25 @@ describe("dreaming markdown storage", () => {
expect(content).toContain("- Candidate: remember the API key is fake");
});
it("falls back when the injected timestamp is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");
const result = await writeDailyDreamingPhaseBlock({
workspaceDir,
phase: "light",
bodyLines: ["- Candidate: bounded fallback"],
nowMs: 8_640_000_000_000_001,
timezone,
storage: {
mode: "inline",
separateReports: false,
},
});
expect(requireInlinePath(result)).toBe(path.join(workspaceDir, "memory", "2026-05-30.md"));
});
it("keeps multiple inline phases in the shared daily memory file", async () => {
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");

View File

@@ -10,6 +10,7 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
const DAILY_PHASE_HEADINGS: Record<Exclude<MemoryDreamingPhaseName, "deep">, string> = {
light: "## Light Sleep",
@@ -63,7 +64,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
timezone?: string;
storage: MemoryDreamingStorageConfig;
}): Promise<{ inlinePath?: string; reportPath?: string }> {
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No notable updates.";
let inlinePath: string | undefined;
let reportPath: string | undefined;
@@ -107,7 +108,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
await appendMemoryHostEvent(params.workspaceDir, {
type: "memory.dream.completed",
timestamp: new Date(nowMs).toISOString(),
timestamp: resolveMemoryCoreTimestamp(nowMs),
phase: params.phase,
...(inlinePath ? { inlinePath } : {}),
...(reportPath ? { reportPath } : {}),
@@ -131,14 +132,14 @@ export async function writeDeepDreamingReport(params: {
if (!shouldWriteSeparate(params.storage)) {
return undefined;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone);
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes.";
await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8");
await appendMemoryHostEvent(params.workspaceDir, {
type: "memory.dream.completed",
timestamp: new Date(nowMs).toISOString(),
timestamp: resolveMemoryCoreTimestamp(nowMs),
phase: "deep",
reportPath,
lineCount: params.bodyLines.length,

View File

@@ -0,0 +1,18 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildMemoryFlushPlan } from "./flush-plan.js";
describe("buildMemoryFlushPlan", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("falls back when the injected timestamp is outside Date range", () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
const plan = buildMemoryFlushPlan({
nowMs: 8_640_000_000_000_001,
});
expect(plan?.relativePath).toBe("memory/2026-05-30.md");
});
});

View File

@@ -6,6 +6,7 @@ import {
type MemoryFlushPlan,
type OpenClawConfig,
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { resolveMemoryCoreNowMs } from "./time.js";
export const DEFAULT_MEMORY_FLUSH_SOFT_TOKENS = 4000;
export const DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
@@ -53,7 +54,7 @@ function formatDateStampInTimezone(nowMs: number, timezone: string): string {
if (year && month && day) {
return `${year}-${month}-${day}`;
}
return new Date(nowMs).toISOString().slice(0, 10);
return new Date(resolveMemoryCoreNowMs(nowMs)).toISOString().slice(0, 10);
}
function normalizeNonNegativeInt(value: unknown): number | null {
@@ -99,7 +100,7 @@ export function buildMemoryFlushPlan(
} = {},
): MemoryFlushPlan | null {
const resolved = params;
const nowMs = Number.isFinite(resolved.nowMs) ? (resolved.nowMs as number) : Date.now();
const nowMs = resolveMemoryCoreNowMs(resolved.nowMs);
const cfg = resolved.cfg;
const defaults = cfg?.agents?.defaults?.compaction?.memoryFlush;
if (defaults?.enabled === false) {

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
appendMemoryHostEvent: vi.fn(async () => {}),
@@ -40,6 +40,10 @@ describe("short-term promotion", () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
afterEach(() => {
vi.restoreAllMocks();
});
async function withTempWorkspace(run: (workspaceDir: string) => Promise<void>) {
const workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
await fs.mkdir(path.join(workspaceDir, "memory", ".dreams"), { recursive: true });
@@ -89,19 +93,31 @@ describe("short-term promotion", () => {
return candidate.promotedAt;
}
async function readRecallStoreEntries(
workspaceDir: string,
): Promise<
async function readRecallStoreEntries(workspaceDir: string): Promise<
Record<
string,
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
{
claimHash?: unknown;
firstRecalledAt?: unknown;
lastRecalledAt?: unknown;
recallCount?: unknown;
snippet?: unknown;
totalScore?: unknown;
}
>
> {
const raw = await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8");
const store = JSON.parse(raw) as {
entries?: Record<
string,
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
{
claimHash?: unknown;
firstRecalledAt?: unknown;
lastRecalledAt?: unknown;
recallCount?: unknown;
snippet?: unknown;
totalScore?: unknown;
}
>;
};
return store.entries ?? {};
@@ -160,6 +176,35 @@ describe("short-term promotion", () => {
});
});
it("falls back when the injected recall timestamp is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
await withTempWorkspace(async (workspaceDir) => {
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-05-30", [
"Bounded recall timestamp note.",
]);
await recordShortTermRecalls({
workspaceDir,
query: "bounded recall",
nowMs: 8_640_000_000_000_001,
results: [
{
path: path.relative(workspaceDir, notePath).replaceAll("\\", "/"),
source: "memory",
startLine: 1,
endLine: 1,
score: 0.9,
snippet: "Bounded recall timestamp note.",
},
],
});
const [entry] = Object.values(await readRecallStoreEntries(workspaceDir));
expect(entry?.firstRecalledAt).toBe("2026-05-30T12:00:00.000Z");
expect(entry?.lastRecalledAt).toBe("2026-05-30T12:00:00.000Z");
});
});
it("records short-term recall for notes stored in spaced and Unicode memory subdirectories", async () => {
await withTempWorkspace(async (workspaceDir) => {
const spacedPath = await writeDailyMemoryNoteInSubdir(

View File

@@ -21,6 +21,7 @@ import {
} from "./concept-vocabulary.js";
import { asRecord } from "./dreaming-shared.js";
import { compactMemoryForBudget, DEFAULT_MEMORY_FILE_MAX_CHARS } from "./memory-budget.js";
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(?:[^/]+\/)*(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/;
const DREAMING_MEMORY_PATH_RE = /(?:^|\/)memory\/dreaming\//;
@@ -1052,8 +1053,8 @@ export async function recordShortTermRecalls(params: {
return;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const signalType = params.signalType ?? "recall";
const queryHash = hashQuery(query);
const todayBucket =
@@ -1199,8 +1200,8 @@ export async function recordGroundedShortTermCandidates(params: {
return;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const fallbackDayBucket = formatMemoryDreamingDay(nowMs, params.timezone);
await withShortTermLock(workspaceDir, async () => {
const store = await readStore(workspaceDir, nowIso);
@@ -1281,8 +1282,8 @@ export async function recordDreamingPhaseSignals(params: {
if (keys.length === 0) {
return;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([
@@ -1334,8 +1335,8 @@ export async function recordRemConsideredPhaseSignals(params: {
if (keys.length === 0) {
return;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([
@@ -1376,8 +1377,8 @@ export async function readLightStagedKeys(params: {
if (!workspaceDir) {
return new Set();
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const store = await readPhaseSignalStore(workspaceDir, nowIso);
const keys = new Set<string>();
for (const [key, entry] of Object.entries(store.entries)) {
@@ -1409,8 +1410,8 @@ export async function rankShortTermPromotionCandidates(
return [];
}
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const minScore = toFiniteScore(options.minScore, DEFAULT_PROMOTION_MIN_SCORE);
const minRecallCount = toFiniteNonNegativeInt(
options.minRecallCount,
@@ -1550,8 +1551,8 @@ export async function readShortTermRecallEntries(params: {
if (!workspaceDir) {
return [];
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const store = await readStore(workspaceDir, nowIso);
return Object.values(store.entries).filter(
(entry): entry is ShortTermRecallEntry =>
@@ -1838,8 +1839,8 @@ export async function applyShortTermPromotions(
options: ApplyShortTermPromotionsOptions,
): Promise<ApplyShortTermPromotionsResult> {
const workspaceDir = options.workspaceDir.trim();
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const limit = Number.isFinite(options.limit)
? Math.max(0, Math.floor(options.limit as number))
: options.candidates.length;

View File

@@ -0,0 +1,10 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
export function resolveMemoryCoreNowMs(nowMs: unknown): number {
return timestampMsToIsoString(nowMs) === undefined ? Date.now() : (nowMs as number);
}
export function resolveMemoryCoreTimestamp(nowMs: unknown): string {
const timestampMs = resolveMemoryCoreNowMs(nowMs);
return timestampMsToIsoString(timestampMs) ?? new Date().toISOString();
}

View File

@@ -6,6 +6,7 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
@@ -16,6 +17,7 @@ import {
WIKI_RELATED_END_MARKER,
WIKI_RELATED_START_MARKER,
} from "./markdown.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
import { initializeMemoryWikiVault } from "./vault.js";
const CHATGPT_PREFERENCE_SIGNAL_RE =
@@ -203,7 +205,7 @@ function isoFromUnix(raw: unknown): string | undefined {
if (!Number.isFinite(numeric)) {
return undefined;
}
return new Date(numeric * 1000).toISOString();
return timestampMsToIsoString(numeric * 1000);
}
function cleanMessageText(value: string): string {
@@ -745,7 +747,7 @@ export async function importChatGptConversations(params: {
let updatedCount = 0;
let skippedCount = 0;
let runId: string | undefined;
const nowIso = new Date(params.nowMs ?? Date.now()).toISOString();
const nowIso = resolveMemoryWikiTimestamp(params.nowMs);
let importRunRecord: ChatGptImportRunRecord | undefined;
let importRunDir = "";

View File

@@ -605,4 +605,30 @@ cli note
.then((entries) => entries.filter((entry) => entry !== "index.md")),
).resolves.toStrictEqual([]);
});
it("imports ChatGPT exports with out-of-range Unix timestamps", async () => {
const { rootDir, config } = await createCliVault({ initialize: true });
const exportDir = await createChatGptExport(rootDir);
const conversationsPath = path.join(exportDir, "conversations.json");
const conversations = JSON.parse(await fs.readFile(conversationsPath, "utf8")) as Array<
Record<string, unknown>
>;
conversations[0].update_time = 9_000_000_000_000;
await fs.writeFile(conversationsPath, `${JSON.stringify(conversations, null, 2)}\n`, "utf8");
const result = await runWikiChatGptImport({
config,
exportPath: exportDir,
json: true,
});
expect(result.createdCount).toBe(1);
const sourceFile = (await fs.readdir(path.join(rootDir, "sources"))).find(
(entry) => entry !== "index.md",
);
expect(sourceFile).toBeDefined();
const pageContent = await fs.readFile(path.join(rootDir, "sources", sourceFile ?? ""), "utf8");
expect(pageContent).toContain("- Created: 2024-04-06T00:26:40.000Z");
expect(pageContent).toContain("- Updated: 2024-04-06T00:26:40.000Z");
});
});

View File

@@ -5,6 +5,7 @@ import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
import { renderMarkdownFence, renderWikiMarkdown, slugifyWikiSegment } from "./markdown.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
import { initializeMemoryWikiVault } from "./vault.js";
type IngestMemoryWikiSourceResult = {
@@ -48,7 +49,7 @@ export async function ingestMemoryWikiSource(params: {
const pageRelativePath = path.join("sources", `${slug}.md`);
const pagePath = path.join(params.config.vault.path, pageRelativePath);
const created = !(await pathExists(pagePath));
const timestamp = new Date(params.nowMs ?? Date.now()).toISOString();
const timestamp = resolveMemoryWikiTimestamp(params.nowMs);
const markdown = renderWikiMarkdown({
frontmatter: {

View File

@@ -0,0 +1,48 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeImportedSourcePage } from "./source-page-shared.js";
describe("writeImportedSourcePage", () => {
let suiteRoot: string;
beforeEach(async () => {
suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-source-page-"));
});
afterEach(async () => {
vi.useRealTimers();
await fs.rm(suiteRoot, { recursive: true, force: true });
});
it("falls back when the source mtime is outside the Date range", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-01T12:00:00.000Z"));
const sourcePath = path.join(suiteRoot, "source.txt");
await fs.writeFile(sourcePath, "source body", "utf8");
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
entries: {},
version: 1,
};
const result = await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "unsafe:source",
sourcePath,
sourceUpdatedAtMs: 8_700_000_000_000_000,
sourceSize: 11,
renderFingerprint: "fingerprint",
pagePath: "pages/source.md",
group: "unsafe-local",
state,
buildRendered: (raw, updatedAt) => `updatedAt: ${updatedAt}\n${raw}`,
});
await expect(fs.readFile(path.join(suiteRoot, "pages/source.md"), "utf8")).resolves.toBe(
"updatedAt: 2026-05-01T12:00:00.000Z\nsource body",
);
expect(result).toEqual({ pagePath: "pages/source.md", changed: true, created: true });
expect(state.entries["unsafe:source"]?.sourceUpdatedAtMs).toBe(8_700_000_000_000_000);
});
});

View File

@@ -1,4 +1,5 @@
import fs from "node:fs/promises";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { FsSafeError, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import {
setImportedSourceEntry,
@@ -48,7 +49,7 @@ export async function writeImportedSourcePage(params: {
throw error;
});
const created = !pageStat;
const updatedAt = new Date(params.sourceUpdatedAtMs).toISOString();
const updatedAt = timestampMsToIsoString(params.sourceUpdatedAtMs) ?? new Date().toISOString();
const shouldSkip = await shouldSkipImportedSourceWrite({
vaultRoot: params.vaultRoot,
syncKey: params.syncKey,

View File

@@ -0,0 +1,20 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveMemoryWikiTimestamp } from "./time.js";
describe("resolveMemoryWikiTimestamp", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("uses valid injected timestamps", () => {
expect(resolveMemoryWikiTimestamp(Date.UTC(2026, 3, 5, 12, 0, 0))).toBe(
"2026-04-05T12:00:00.000Z",
);
});
it("falls back when injected timestamps are outside Date range", () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
expect(resolveMemoryWikiTimestamp(8_640_000_000_000_001)).toBe("2026-05-30T12:00:00.000Z");
});
});

View File

@@ -0,0 +1,7 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
export function resolveMemoryWikiTimestamp(nowMs?: number): string {
return (
timestampMsToIsoString(nowMs) ?? timestampMsToIsoString(Date.now()) ?? new Date().toISOString()
);
}

View File

@@ -7,6 +7,7 @@ import {
import { FsSafeError, pathExists, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
export const WIKI_VAULT_DIRECTORIES = [
"entities",
@@ -128,7 +129,7 @@ export async function initializeMemoryWikiVault(
JSON.stringify(
{
version: 1,
createdAt: new Date(options?.nowMs ?? Date.now()).toISOString(),
createdAt: resolveMemoryWikiTimestamp(options?.nowMs),
renderMode: config.vault.renderMode,
},
null,
@@ -142,7 +143,7 @@ export async function initializeMemoryWikiVault(
if (createdDirectories.length > 0 || createdFiles.length > 0) {
await appendMemoryWikiLog(rootDir, {
type: "init",
timestamp: new Date(options?.nowMs ?? Date.now()).toISOString(),
timestamp: resolveMemoryWikiTimestamp(options?.nowMs),
details: {
createdDirectories: createdDirectories.map((dir) => path.relative(rootDir, dir) || "."),
createdFiles: createdFiles.map((file) => path.relative(rootDir, file)),

View File

@@ -22,6 +22,13 @@ import {
} from "../policy-state.js";
import { POLICY_TOOL_GROUPS } from "../tool-policy-conformance.js";
let fsPromisesModulePromise: Promise<typeof import("node:fs/promises")> | null = null;
const loadFsPromisesModule = async () => {
fsPromisesModulePromise ??= import("node:fs/promises");
return await fsPromisesModulePromise;
};
const CHECK_IDS = {
policyAttestationMismatch: "policy/attestation-hash-mismatch",
policyDeniedChannelProvider: "policy/channels-denied-provider",
@@ -5268,7 +5275,7 @@ async function readPolicyFile(
const displayName = policyDisplayName(ctx);
const path = resolveWorkspacePath(ctx, policyPathSetting(ctx));
try {
const fs = await import("node:fs/promises");
const fs = await loadFsPromisesModule();
return {
raw: await fs.readFile(path, "utf-8"),
path,
@@ -5289,7 +5296,7 @@ async function readWorkspaceFile(
): Promise<{ raw: string; path: string } | null> {
const path = resolveWorkspacePath(ctx, fileName);
try {
const fs = await import("node:fs/promises");
const fs = await loadFsPromisesModule();
return { raw: await fs.readFile(path, "utf-8"), path };
} catch (err) {
if (isNotFound(err)) {

View File

@@ -38,6 +38,14 @@ import {
import type { FetchMediaOptions, FetchMediaResult } from "../engine/adapter/types.js";
import { getBridgeLogger } from "./logger.js";
let mediaRuntimeModulePromise: Promise<typeof import("openclaw/plugin-sdk/media-runtime")> | null =
null;
const loadMediaRuntimeModule = async () => {
mediaRuntimeModulePromise ??= import("openclaw/plugin-sdk/media-runtime");
return await mediaRuntimeModulePromise;
};
function createBuiltinAdapter(): PlatformAdapter {
return {
async validateRemoteUrl(_url: string, _options?: { allowPrivate?: boolean }): Promise<void> {
@@ -52,7 +60,7 @@ function createBuiltinAdapter(): PlatformAdapter {
},
async downloadFile(url: string, destDir: string, filename?: string): Promise<string> {
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
const result = await readRemoteMediaBuffer({ url, filePathHint: filename });
const fs = await import("node:fs");
const path = await import("node:path");
@@ -65,7 +73,7 @@ function createBuiltinAdapter(): PlatformAdapter {
},
async fetchMedia(options: FetchMediaOptions): Promise<FetchMediaResult> {
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
const result = await readRemoteMediaBuffer({
url: options.url,
filePathHint: options.filePathHint,

View File

@@ -37,6 +37,14 @@ function loadGatewayModule(): Promise<typeof import("./bridge/gateway.js")> {
return gatewayModulePromise;
}
let outboundMessagingModulePromise:
| Promise<typeof import("./engine/messaging/outbound.js")>
| undefined;
function loadOutboundMessagingModule(): Promise<typeof import("./engine/messaging/outbound.js")> {
outboundMessagingModulePromise ??= import("./engine/messaging/outbound.js");
return outboundMessagingModulePromise;
}
function createQQBotSendReceipt(params: {
messageId?: string;
target: string;
@@ -69,7 +77,7 @@ async function sendQQBotText(params: {
// platform adapter, etc.) have executed before engine code runs.
await loadGatewayModule();
const account = resolveQQBotAccount(params.cfg, params.accountId);
const { sendText } = await import("./engine/messaging/outbound.js");
const { sendText } = await loadOutboundMessagingModule();
const result = await sendText({
to: params.to,
text: params.text,
@@ -100,7 +108,7 @@ async function sendQQBotMedia(params: {
// Same guard as sendText — ensure adapters are registered.
await loadGatewayModule();
const account = resolveQQBotAccount(params.cfg, params.accountId);
const { sendMedia } = await import("./engine/messaging/outbound.js");
const { sendMedia } = await loadOutboundMessagingModule();
const result = await sendMedia({
to: params.to,
text: params.text ?? "",

View File

@@ -86,4 +86,25 @@ describe("QQBot token manager", () => {
expiresAt: Date.now(),
});
});
it("does not throw while logging fetched tokens when the process clock is outside the Date range", async () => {
const logger = { debug: vi.fn(), info: vi.fn(), error: vi.fn() };
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', {
status: 200,
headers: { "content-type": "application/json" },
});
const manager = new TokenManager({ logger });
try {
await expect(manager.getAccessToken("app-id", "secret")).resolves.toBe("token-1");
} finally {
dateNowSpy.mockRestore();
}
expect(manager.getStatus("app-id").expiresAt).toBe(7_200_000);
expect(logger.debug).toHaveBeenCalledWith(
"[qqbot:token:app-id] Cached, expires at: 1970-01-01T02:00:00.000Z",
);
});
});

View File

@@ -6,7 +6,12 @@
* globals, fully supporting multi-account concurrent operation.
*/
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import {
parseStrictPositiveInteger,
resolveDateTimestampMs,
resolveExpiresAtMsFromDurationSeconds,
resolveTimestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import type { EngineLogger } from "../types.js";
import { formatErrorMessage } from "../utils/format.js";
@@ -273,10 +278,14 @@ export class TokenManager {
throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
}
const expiresAt = Date.now() + resolveTokenExpiresInSeconds(data.expires_in) * 1000;
const nowMs = resolveDateTimestampMs(Date.now());
const expiresAt =
resolveExpiresAtMsFromDurationSeconds(resolveTokenExpiresInSeconds(data.expires_in), {
nowMs,
}) ?? nowMs;
this.cache.set(appId, { token: data.access_token, expiresAt, appId });
this.logger?.debug?.(
`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`,
`[qqbot:token:${appId}] Cached, expires at: ${resolveTimestampMsToIsoString(expiresAt)}`,
);
return data.access_token;

View File

@@ -4,7 +4,10 @@ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
import { resolveCommandAuthorization } from "openclaw/plugin-sdk/command-auth-native";
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
import {
parseStrictFiniteNumber,
timestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import {
normalizeOptionalString,
normalizeUniqueTrimmedStringList,
@@ -319,7 +322,10 @@ function formatInteractionSelectionLabel(params: {
return params.summary.selectedTime;
}
if (typeof params.summary.selectedDateTime === "number") {
return new Date(params.summary.selectedDateTime * 1000).toISOString();
const selectedDateTime = timestampMsToIsoString(params.summary.selectedDateTime * 1000);
if (selectedDateTime) {
return selectedDateTime;
}
}
if (params.summary.richTextPreview) {
return params.summary.richTextPreview;

View File

@@ -2075,6 +2075,54 @@ describe("registerSlackInteractionEvents", () => {
expect(payload.selectedDateTime).toBe(1_771_700_200);
});
it("falls back when Slack datetime selection is outside Date range", async () => {
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
const ack = vi.fn().mockResolvedValue(undefined);
await handler({
ack,
body: {
user: { id: "U333" },
channel: { id: "C3" },
message: {
ts: "555.669",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "datetime_block",
elements: [{ type: "datetimepicker", action_id: "openclaw:datetime" }],
},
],
},
},
action: {
type: "datetimepicker",
action_id: "openclaw:datetime",
block_id: "datetime_block",
selected_date_time: 9_000_000_000_000,
},
});
expectRecordFields(chatUpdateCall(app), {
channel: "C3",
ts: "555.669",
blocks: [
{
type: "context",
elements: [
{
type: "mrkdwn",
text: ":white_check_mark: *openclaw:datetime* selected by <@U333>",
},
],
},
],
});
});
it("captures workflow button trigger metadata", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getHandler } = createContext();

View File

@@ -583,6 +583,37 @@ describe("voice-call plugin", () => {
expect(runtimeStub.manager.speak).not.toHaveBeenCalled();
});
it("reports stale call history with invalid ended timestamps", async () => {
runtimeStub.manager.getCall = vi.fn(() => undefined);
runtimeStub.manager.getCallByProviderCallId = vi.fn(() => undefined);
runtimeStub.manager.getCallHistory = vi.fn(async () => [
createCallRecord({
callId: "call-1",
providerCallId: "CA123",
state: "completed",
endReason: "completed",
endedAt: Number.POSITIVE_INFINITY,
}),
]);
const { methods } = setup({ provider: "mock" });
const handler = methods.get("voicecall.speak") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({ params: { callId: "CA123", message: "hello" }, respond });
const [ok, , error] = firstRespondCall(respond);
expect(ok).toBe(false);
expect(error?.message).toContain("call is not active");
expect(error?.message).toContain("last state=completed");
expect(error?.message).toContain("endReason=completed");
expect(error?.message).not.toContain("endedAt=");
});
it("normalizes legacy config through runtime creation and warns to run doctor", async () => {
const { methods } = setup({
enabled: true,

View File

@@ -1,5 +1,6 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import {
@@ -347,10 +348,11 @@ export default definePluginEntry({
if (!call) {
return undefined;
}
const endedAt = timestampMsToIsoString(call.endedAt);
const details = [
`last state=${call.state}`,
call.endReason ? `endReason=${call.endReason}` : undefined,
call.endedAt ? `endedAt=${new Date(call.endedAt).toISOString()}` : undefined,
endedAt ? `endedAt=${endedAt}` : undefined,
].filter(Boolean);
return `call is not active (${details.join(", ")})`;
};

545
npm-shrinkwrap.json generated
View File

@@ -74,7 +74,6 @@
"node": ">=22.19.0"
},
"optionalDependencies": {
"sharp": "0.34.5",
"sqlite-vec": "0.1.9"
}
},
@@ -168,16 +167,6 @@
"node": ">=22.19.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@google/genai": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz",
@@ -265,472 +254,6 @@
"hono": "^4"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -1475,16 +998,6 @@
"node": ">= 0.8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/diff": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz",
@@ -3238,19 +2751,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
@@ -3314,51 +2814,6 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -1941,7 +1941,6 @@
"vitest": "4.1.7"
},
"optionalDependencies": {
"sharp": "0.34.5",
"sqlite-vec": "0.1.9"
},
"overrides": {

View File

@@ -0,0 +1,34 @@
{
"name": "@openclaw/code-mode-runtime",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"default": "./dist/types.mjs"
},
"./worker": {
"types": "./dist/worker.d.mts",
"import": "./dist/worker.mjs",
"default": "./dist/worker.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/types.ts src/worker.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
},
"dependencies": {
"quickjs-wasi": "3.0.0"
}
}

View File

@@ -0,0 +1,18 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export type {
CodeModeBridgeMethod,
CodeModeFailureCode,
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
export function resolveCodeModeRuntimeWorkerUrl(currentModuleUrl = import.meta.url): URL {
const currentPath = fileURLToPath(currentModuleUrl);
const extension = path.extname(currentPath) || ".js";
return new URL(`./worker${extension}`, currentModuleUrl);
}

View File

@@ -0,0 +1,62 @@
export type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
export type CodeModeRuntimeConfig = {
timeoutMs: number;
memoryLimitBytes: number;
maxPendingToolCalls: number;
maxSnapshotBytes: number;
};
export type CodeModePendingBridgeRequest = {
id: string;
method: CodeModeBridgeMethod;
args: unknown[];
};
export type CodeModeSettledBridgeRequest = {
id: string;
ok: boolean;
value?: unknown;
error?: string;
};
export type CodeModeWorkerInput =
| {
kind: "exec";
source: string;
config: CodeModeRuntimeConfig;
catalog: unknown[];
}
| {
kind: "resume";
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
settledRequests: CodeModeSettledBridgeRequest[];
};
export type CodeModeFailureCode =
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
export type CodeModeWorkerResult =
| {
status: "completed";
value: unknown;
output: unknown[];
}
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
}
| {
status: "failed";
error: string;
code: CodeModeFailureCode;
output: unknown[];
};

View File

@@ -0,0 +1,555 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { parentPort, workerData } from "node:worker_threads";
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
import type {
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
const require = createRequire(import.meta.url);
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
class CodeModeWorkerFailure extends Error {
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "CodeModeWorkerFailure";
this.code = code;
}
}
class CodeModeWorkerFailureWithOutput extends CodeModeWorkerFailure {
readonly output: unknown[];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
output: unknown[],
options?: ErrorOptions,
) {
super(code, message, options);
this.name = "CodeModeWorkerFailureWithOutput";
this.output = output;
}
}
class CodeModeGuestError extends Error {
constructor(message: string) {
super(message);
this.name = "CodeModeGuestError";
}
}
function isQuickJsInterruptedError(error: unknown): boolean {
if (error instanceof CodeModeGuestError) {
return false;
}
return errorMessage(error) === "interrupted";
}
type VmRun = {
vm: QuickJS;
didTimeout: () => boolean;
};
function getQuickJsWasmModule(): Promise<WebAssembly.Module> {
quickJsWasmModulePromise ??= readFile(QUICKJS_WASM_PATH).then((bytes) =>
WebAssembly.compile(bytes),
);
return quickJsWasmModulePromise;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function toJsonSafe(value: unknown): unknown {
if (value === undefined) {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) {
return null;
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
return value;
case "bigint":
case "symbol":
case "function":
return String(value);
default:
return Object.prototype.toString.call(value);
}
}
}
function errorMessage(error: unknown): string {
if (error instanceof JSException) {
return error.stack || error.message || String(error);
}
if (error instanceof Error) {
return error.message || String(error);
}
return String(error);
}
const CONTROLLER_SOURCE = String.raw`
(() => {
const output = [];
const pending = new Map();
const catalog = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : [];
function safe(value) {
if (value === undefined) return null;
try {
return JSON.parse(JSON.stringify(value));
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) return null;
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") return value;
return String(value);
}
}
function asText(value) {
if (typeof value === "string") return value;
const encoded = JSON.stringify(safe(value));
return typeof encoded === "string" ? encoded : String(value);
}
function request(method, args) {
const id = String(globalThis.__openclawHostRequest(String(method), JSON.stringify(safe(args ?? []))));
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
}
function settle(id, ok, payload) {
const entry = pending.get(String(id));
if (!entry) return false;
pending.delete(String(id));
let parsed = null;
try {
parsed = JSON.parse(String(payload));
} catch {
parsed = String(payload);
}
if (ok) {
entry.resolve(parsed);
} else {
const error = new Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
entry.reject(error);
}
return true;
}
const baseTools = Object.create(null);
Object.defineProperties(baseTools, {
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
describe: { value: (id) => request("describe", [id]), enumerable: true },
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
});
const safeNameCounts = new Map();
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
}
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
const id = typeof tool?.id === "string" ? tool.id : "";
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
continue;
}
Object.defineProperty(baseTools, name, {
value: (input) => request("call", [id, input]),
enumerable: true,
});
}
Object.defineProperties(globalThis, {
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
tools: { value: Object.freeze(baseTools), enumerable: true },
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
__openclawSettleBridge: { value: settle },
__openclawTakeOutput: { value: () => output.splice(0) },
});
})();
`;
function buildUserSource(code: string): string {
return `globalThis.__openclawResult = (async () => {\n${code}\n})()`;
}
function createHostRequestHandler(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
config: CodeModeRuntimeConfig;
}): (this: JSValueHandle, method: JSValueHandle, argsJson: JSValueHandle) => JSValueHandle {
return (methodHandle, argsHandle) => {
if (params.pendingRequests.length >= params.config.maxPendingToolCalls) {
throw new Error("too many pending code mode tool calls");
}
const method = methodHandle.toString();
if (method !== "search" && method !== "describe" && method !== "call" && method !== "yield") {
throw new Error("unsupported code mode bridge method");
}
let args: unknown = [];
try {
args = JSON.parse(argsHandle.toString()) as unknown;
} catch {
args = [];
}
const id = `bridge:${params.pendingRequests.length + 1}:${randomUUID()}`;
params.pendingRequests.push({
id,
method,
args: Array.isArray(args) ? args : [],
});
return params.vm.newString(id);
};
}
async function createVm(params: {
catalog: unknown[];
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const vm = await QuickJS.create({
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
const catalogHandle = vm.hostToHandle(params.catalog);
try {
vm.setProp(vm.global, "__openclawCatalog", catalogHandle);
} finally {
catalogHandle.dispose();
}
const hostRequest = vm.newFunction(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
try {
vm.setProp(vm.global, "__openclawHostRequest", hostRequest);
} finally {
hostRequest.dispose();
}
vm.evalCode(CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
return { vm, didTimeout: () => timedOut };
}
async function restoreVm(params: {
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const snapshot = QuickJS.deserializeSnapshot(params.snapshotBytes);
const vm = await QuickJS.restore(snapshot, {
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
vm.registerHostCallback(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
return { vm, didTimeout: () => timedOut };
}
function takeOutput(vm: QuickJS): unknown[] {
const take = vm.global.getProp("__openclawTakeOutput");
try {
const output = vm.callFunction(take, vm.undefined);
try {
const dumped = vm.dump(output);
return Array.isArray(dumped) ? (dumped as unknown[]) : [];
} finally {
output.dispose();
}
} finally {
take.dispose();
}
}
function takeOutputSafely(vm: QuickJS): unknown[] {
try {
return takeOutput(vm);
} catch {
return [];
}
}
function throwWorkerFailureWithOutput(params: {
error: unknown;
didTimeout: () => boolean;
output: unknown[];
vm: QuickJS;
}): never {
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
const failureOutput = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
if (timedOut) {
throw new CodeModeWorkerFailureWithOutput(
"timeout",
"code mode timeout exceeded",
failureOutput,
{ cause: params.error },
);
}
if (params.error instanceof CodeModeWorkerFailure) {
throw new CodeModeWorkerFailureWithOutput(
params.error.code,
params.error.message,
failureOutput,
{ cause: params.error },
);
}
if (failureOutput.length > 0) {
throw new CodeModeWorkerFailureWithOutput(
"internal_error",
errorMessage(params.error),
failureOutput,
{ cause: params.error },
);
}
throw params.error;
}
function drainPendingJobs(vm: QuickJS): void {
for (let index = 0; index < 1000; index += 1) {
if (vm.executePendingJobs() === 0) {
return;
}
}
throw new Error("code mode pending job limit exceeded");
}
function getResultHandle(vm: QuickJS): JSValueHandle {
return vm.global.getProp("__openclawResult");
}
async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Promise<unknown> {
if (!resultHandle.isPromise) {
return toJsonSafe(vm.dump(resultHandle));
}
const settled = await vm.resolvePromise(resultHandle);
if ("error" in settled) {
try {
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
} finally {
settled.error.dispose();
}
}
try {
return toJsonSafe(vm.dump(settled.value));
} finally {
settled.value.dispose();
}
}
function waitingResult(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
config: CodeModeRuntimeConfig;
}): CodeModeWorkerResult {
const snapshotBytes = QuickJS.serializeSnapshot(params.vm.snapshot());
if (snapshotBytes.byteLength > params.config.maxSnapshotBytes) {
throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
}
return {
status: "waiting",
snapshotBytes,
pendingRequests: params.pendingRequests,
output: params.output,
};
}
async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await createVm({
catalog: input.catalog,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
vm.evalCode(
buildUserSource(input.source),
"openclaw-code-mode:user.js",
EvalFlags.ASYNC,
).dispose();
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await restoreVm({
snapshotBytes: input.snapshotBytes,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
const settle = vm.global.getProp("__openclawSettleBridge");
try {
for (const request of input.settledRequests) {
const id = vm.newString(request.id);
const payload = vm.newString(JSON.stringify(request.ok ? request.value : request.error));
try {
vm.callFunction(
settle,
vm.undefined,
id,
request.ok ? vm.true : vm.false,
payload,
).dispose();
} finally {
id.dispose();
payload.dispose();
}
}
} finally {
settle.dispose();
}
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function main(): Promise<CodeModeWorkerResult> {
const input = workerData as unknown;
if (!isRecord(input) || !isRecord(input.config)) {
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
}
try {
if (input.kind === "exec" && typeof input.source === "string") {
return await runExec({
kind: "exec",
source: input.source,
config: input.config as CodeModeRuntimeConfig,
catalog: Array.isArray(input.catalog) ? input.catalog : [],
});
}
if (input.kind === "resume" && input.snapshotBytes instanceof Uint8Array) {
return await runResume({
kind: "resume",
snapshotBytes: input.snapshotBytes,
config: input.config as CodeModeRuntimeConfig,
settledRequests: Array.isArray(input.settledRequests)
? (input.settledRequests as CodeModeSettledBridgeRequest[])
: [],
});
}
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
} catch (error) {
return {
status: "failed",
error: errorMessage(error),
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
};
}
}
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node worker_threads MessagePort, not window.postMessage.
parentPort?.postMessage(await main());

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,7 @@
//#region packages/media-understanding-common/src/active-model.d.ts
type ActiveMediaModel = {
provider: string;
model?: string;
};
//#endregion
export { ActiveMediaModel };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,14 @@
import { MediaUnderstandingCapability } from "./types.mjs";
//#region packages/media-understanding-common/src/defaults.d.ts
declare const DEFAULT_MAX_CHARS = 500;
declare const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<MediaUnderstandingCapability, number | undefined>;
declare const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number>;
declare const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number>;
declare const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string>;
declare const DEFAULT_VIDEO_MAX_BASE64_BYTES: number;
declare const CLI_OUTPUT_MAX_BUFFER: number;
declare const DEFAULT_MEDIA_CONCURRENCY = 2;
declare const MIN_AUDIO_FILE_BYTES = 1024;
//#endregion
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };

View File

@@ -0,0 +1,29 @@
//#region packages/media-understanding-common/src/defaults.ts
const MB = 1024 * 1024;
const DEFAULT_MAX_CHARS = 500;
const DEFAULT_MAX_CHARS_BY_CAPABILITY = {
image: 500,
audio: void 0,
video: 500
};
const DEFAULT_MAX_BYTES = {
image: 10 * MB,
audio: 20 * MB,
video: 50 * MB
};
const DEFAULT_TIMEOUT_SECONDS = {
image: 60,
audio: 60,
video: 120
};
const DEFAULT_PROMPT = {
image: "Describe the image.",
audio: "Transcribe the audio.",
video: "Describe the video."
};
const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
const DEFAULT_MEDIA_CONCURRENCY = 2;
const MIN_AUDIO_FILE_BYTES = 1024;
//#endregion
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };

View File

@@ -0,0 +1,9 @@
//#region packages/media-understanding-common/src/errors.d.ts
type MediaUnderstandingSkipReason = "maxBytes" | "timeout" | "unsupported" | "empty" | "blocked" | "tooSmall";
declare class MediaUnderstandingSkipError extends Error {
readonly reason: MediaUnderstandingSkipReason;
constructor(reason: MediaUnderstandingSkipReason, message: string);
}
declare function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError;
//#endregion
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };

View File

@@ -0,0 +1,13 @@
//#region packages/media-understanding-common/src/errors.ts
var MediaUnderstandingSkipError = class extends Error {
constructor(reason, message) {
super(message);
this.reason = reason;
this.name = "MediaUnderstandingSkipError";
}
};
function isMediaUnderstandingSkipError(err) {
return err instanceof MediaUnderstandingSkipError;
}
//#endregion
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };

View File

@@ -0,0 +1,11 @@
import { MediaUnderstandingOutput } from "./types.mjs";
//#region packages/media-understanding-common/src/format.d.ts
declare function extractMediaUserText(body?: string): string | undefined;
declare function formatMediaUnderstandingBody(params: {
body?: string;
outputs: MediaUnderstandingOutput[];
}): string;
declare function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string;
//#endregion
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };

View File

@@ -0,0 +1,47 @@
//#region packages/media-understanding-common/src/format.ts
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
function extractMediaUserText(body) {
const trimmed = body?.trim() ?? "";
if (!trimmed) return;
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return;
return trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim() || void 0;
}
function formatSection(title, kind, text, userText) {
const lines = [`[${title}]`];
if (userText) lines.push(`User text:\n${userText}`);
lines.push(`${kind}:\n${text}`);
return lines.join("\n");
}
function formatMediaUnderstandingBody(params) {
const outputs = params.outputs.filter((output) => output.text.trim());
if (outputs.length === 0) return params.body ?? "";
const userText = extractMediaUserText(params.body);
const sections = [];
if (userText && outputs.length > 1) sections.push(`User text:\n${userText}`);
const counts = /* @__PURE__ */ new Map();
for (const output of outputs) counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
const seen = /* @__PURE__ */ new Map();
for (const output of outputs) {
const count = counts.get(output.kind) ?? 1;
const next = (seen.get(output.kind) ?? 0) + 1;
seen.set(output.kind, next);
const suffix = count > 1 ? ` ${next}/${count}` : "";
if (output.kind === "audio.transcription") {
sections.push(formatSection(`Audio${suffix}`, "Transcript", output.text, outputs.length === 1 ? userText : void 0));
continue;
}
if (output.kind === "image.description") {
sections.push(formatSection(`Image${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
continue;
}
sections.push(formatSection(`Video${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
}
return sections.join("\n\n").trim();
}
function formatAudioTranscripts(outputs) {
if (outputs.length === 1) return outputs[0].text;
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}
//#endregion
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };

View File

@@ -0,0 +1,11 @@
import { ActiveMediaModel } from "./active-model.mjs";
import { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider } from "./types.mjs";
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
import { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
import { extractGeminiResponse } from "./output-extract.mjs";
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
import { providerSupportsCapability } from "./provider-supports.mjs";
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
export { ActiveMediaModel, CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider, MediaUnderstandingSkipError, OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };

View File

@@ -0,0 +1,11 @@
import "./active-model.mjs";
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
import { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
import { extractGeminiResponse } from "./output-extract.mjs";
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
import { providerSupportsCapability } from "./provider-supports.mjs";
import "./types.mjs";
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaUnderstandingSkipError, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };

View File

@@ -0,0 +1,37 @@
//#region packages/media-understanding-common/src/openai-compatible-video.d.ts
type OpenAiCompatibleVideoPayload = {
choices?: Array<{
message?: {
content?: string | Array<{
text?: string;
}>;
reasoning_content?: string;
};
}>;
};
declare function resolveMediaUnderstandingString(value: string | undefined, fallback: string): string;
declare function coerceOpenAiCompatibleVideoText(payload: OpenAiCompatibleVideoPayload): string | null;
declare function buildOpenAiCompatibleVideoRequestBody(params: {
model: string;
prompt: string;
mime: string;
buffer: Buffer;
}): {
model: string;
messages: {
role: string;
content: ({
type: string;
text: string;
video_url?: undefined;
} | {
type: string;
video_url: {
url: string;
};
text?: undefined;
})[];
}[];
};
//#endregion
export { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };

View File

@@ -0,0 +1,32 @@
//#region packages/media-understanding-common/src/openai-compatible-video.ts
function resolveMediaUnderstandingString(value, fallback) {
return value?.trim() || fallback;
}
function coerceOpenAiCompatibleVideoText(payload) {
const message = payload.choices?.[0]?.message;
if (!message) return null;
if (typeof message.content === "string" && message.content.trim()) return message.content.trim();
if (Array.isArray(message.content)) {
const text = message.content.map((part) => part.text?.trim() ?? "").filter(Boolean).join("\n");
if (text) return text;
}
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) return message.reasoning_content.trim();
return null;
}
function buildOpenAiCompatibleVideoRequestBody(params) {
return {
model: params.model,
messages: [{
role: "user",
content: [{
type: "text",
text: params.prompt
}, {
type: "video_url",
video_url: { url: `data:${params.mime};base64,${params.buffer.toString("base64")}` }
}]
}]
};
}
//#endregion
export { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };

View File

@@ -0,0 +1,4 @@
//#region packages/media-understanding-common/src/output-extract.d.ts
declare function extractGeminiResponse(raw: string): string | null;
//#endregion
export { extractGeminiResponse };

View File

@@ -0,0 +1,21 @@
//#region packages/media-understanding-common/src/output-extract.ts
function extractLastJsonObject(raw) {
const trimmed = raw.trim();
const start = trimmed.lastIndexOf("{");
if (start === -1) return null;
const slice = trimmed.slice(start);
try {
return JSON.parse(slice);
} catch {
return null;
}
}
function extractGeminiResponse(raw) {
const payload = extractLastJsonObject(raw);
if (!payload || typeof payload !== "object") return null;
const response = payload.response;
if (typeof response !== "string") return null;
return response.trim() || null;
}
//#endregion
export { extractGeminiResponse };

View File

@@ -0,0 +1,5 @@
//#region packages/media-understanding-common/src/provider-id.d.ts
declare function normalizeMediaProviderId(id: string): string;
declare function normalizeMediaExecutionProviderId(id: string): string;
//#endregion
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };

View File

@@ -0,0 +1,18 @@
//#region packages/media-understanding-common/src/provider-id.ts
function normalizeProviderId(provider) {
return provider.trim().toLowerCase();
}
function normalizeMediaProviderId(id) {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") return "google";
if (normalized === "minimax-cn") return "minimax";
if (normalized === "minimax-portal-cn") return "minimax-portal";
return normalized;
}
function normalizeMediaExecutionProviderId(id) {
const normalized = normalizeProviderId(id);
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") return normalized;
return normalizeMediaProviderId(normalized);
}
//#endregion
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };

View File

@@ -0,0 +1,6 @@
import { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.mjs";
//#region packages/media-understanding-common/src/provider-supports.d.ts
declare function providerSupportsCapability(provider: MediaUnderstandingProvider | undefined, capability: MediaUnderstandingCapability): boolean;
//#endregion
export { providerSupportsCapability };

View File

@@ -0,0 +1,9 @@
//#region packages/media-understanding-common/src/provider-supports.ts
function providerSupportsCapability(provider, capability) {
if (!provider) return false;
if (capability === "audio") return Boolean(provider.transcribeAudio);
if (capability === "image") return Boolean(provider.describeImage);
return Boolean(provider.describeVideo);
}
//#endregion
export { providerSupportsCapability };

View File

@@ -0,0 +1,31 @@
//#region packages/media-understanding-common/src/types.d.ts
type MediaUnderstandingKind = "audio.transcription" | "video.description" | "image.description";
type MediaUnderstandingCapability = "image" | "audio" | "video";
type MediaUnderstandingCapabilityRegistry = Map<string, {
capabilities?: MediaUnderstandingCapability[];
}>;
type MediaAttachment = {
path?: string;
url?: string;
mime?: string;
index: number;
alreadyTranscribed?: boolean;
};
type MediaUnderstandingOutput = {
kind: MediaUnderstandingKind;
attachmentIndex: number;
text: string;
provider: string;
model?: string;
};
type MediaUnderstandingProvider = {
id: string;
capabilities?: MediaUnderstandingCapability[];
transcribeAudio?: unknown;
describeVideo?: unknown;
describeImage?: unknown;
describeImages?: unknown;
extractStructured?: unknown;
};
//#endregion
export { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,5 @@
//#region packages/media-understanding-common/src/video.d.ts
declare function estimateBase64Size(bytes: number): number;
declare function resolveVideoMaxBase64Bytes(maxBytes: number): number;
//#endregion
export { estimateBase64Size, resolveVideoMaxBase64Bytes };

View File

@@ -0,0 +1,11 @@
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.mjs";
//#region packages/media-understanding-common/src/video.ts
function estimateBase64Size(bytes) {
return Math.ceil(bytes / 3) * 4;
}
function resolveVideoMaxBase64Bytes(maxBytes) {
const expanded = Math.floor(maxBytes * (4 / 3));
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
}
//#endregion
export { estimateBase64Size, resolveVideoMaxBase64Bytes };

View File

@@ -0,0 +1,71 @@
{
"name": "@openclaw/media-understanding-common",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./active-model": {
"types": "./dist/active-model.d.mts",
"import": "./dist/active-model.mjs",
"default": "./dist/active-model.mjs"
},
"./defaults": {
"types": "./dist/defaults.d.mts",
"import": "./dist/defaults.mjs",
"default": "./dist/defaults.mjs"
},
"./errors": {
"types": "./dist/errors.d.mts",
"import": "./dist/errors.mjs",
"default": "./dist/errors.mjs"
},
"./format": {
"types": "./dist/format.d.mts",
"import": "./dist/format.mjs",
"default": "./dist/format.mjs"
},
"./openai-compatible-video": {
"types": "./dist/openai-compatible-video.d.mts",
"import": "./dist/openai-compatible-video.mjs",
"default": "./dist/openai-compatible-video.mjs"
},
"./output-extract": {
"types": "./dist/output-extract.d.mts",
"import": "./dist/output-extract.mjs",
"default": "./dist/output-extract.mjs"
},
"./provider-id": {
"types": "./dist/provider-id.d.mts",
"import": "./dist/provider-id.mjs",
"default": "./dist/provider-id.mjs"
},
"./provider-supports": {
"types": "./dist/provider-supports.d.mts",
"import": "./dist/provider-supports.mjs",
"default": "./dist/provider-supports.mjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"default": "./dist/types.mjs"
},
"./video": {
"types": "./dist/video.d.mts",
"import": "./dist/video.mjs",
"default": "./dist/video.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/active-model.ts src/defaults.ts src/errors.ts src/format.ts src/openai-compatible-video.ts src/output-extract.ts src/provider-id.ts src/provider-supports.ts src/types.ts src/video.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View File

@@ -0,0 +1,4 @@
export type ActiveMediaModel = {
provider: string;
model?: string;
};

View File

@@ -0,0 +1,32 @@
import type { MediaUnderstandingCapability } from "./types.js";
const MB = 1024 * 1024;
export const DEFAULT_MAX_CHARS = 500;
export const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<
MediaUnderstandingCapability,
number | undefined
> = {
image: DEFAULT_MAX_CHARS,
audio: undefined,
video: DEFAULT_MAX_CHARS,
};
export const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number> = {
image: 10 * MB,
audio: 20 * MB,
video: 50 * MB,
};
export const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number> = {
image: 60,
audio: 60,
video: 120,
};
export const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string> = {
image: "Describe the image.",
audio: "Transcribe the audio.",
video: "Describe the video.",
};
export const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
export const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
export const DEFAULT_MEDIA_CONCURRENCY = 2;
export const MIN_AUDIO_FILE_BYTES = 1024;

View File

@@ -0,0 +1,21 @@
type MediaUnderstandingSkipReason =
| "maxBytes"
| "timeout"
| "unsupported"
| "empty"
| "blocked"
| "tooSmall";
export class MediaUnderstandingSkipError extends Error {
readonly reason: MediaUnderstandingSkipReason;
constructor(reason: MediaUnderstandingSkipReason, message: string) {
super(message);
this.reason = reason;
this.name = "MediaUnderstandingSkipError";
}
}
export function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError {
return err instanceof MediaUnderstandingSkipError;
}

View File

@@ -0,0 +1,98 @@
import type { MediaUnderstandingOutput } from "./types.js";
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
export function extractMediaUserText(body?: string): string | undefined {
const trimmed = body?.trim() ?? "";
if (!trimmed) {
return undefined;
}
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
return undefined;
}
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
return cleaned || undefined;
}
function formatSection(
title: string,
kind: "Transcript" | "Description",
text: string,
userText?: string,
): string {
const lines = [`[${title}]`];
if (userText) {
lines.push(`User text:\n${userText}`);
}
lines.push(`${kind}:\n${text}`);
return lines.join("\n");
}
export function formatMediaUnderstandingBody(params: {
body?: string;
outputs: MediaUnderstandingOutput[];
}): string {
const outputs = params.outputs.filter((output) => output.text.trim());
if (outputs.length === 0) {
return params.body ?? "";
}
const userText = extractMediaUserText(params.body);
const sections: string[] = [];
if (userText && outputs.length > 1) {
sections.push(`User text:\n${userText}`);
}
const counts = new Map<MediaUnderstandingOutput["kind"], number>();
for (const output of outputs) {
counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
}
const seen = new Map<MediaUnderstandingOutput["kind"], number>();
for (const output of outputs) {
const count = counts.get(output.kind) ?? 1;
const next = (seen.get(output.kind) ?? 0) + 1;
seen.set(output.kind, next);
const suffix = count > 1 ? ` ${next}/${count}` : "";
if (output.kind === "audio.transcription") {
sections.push(
formatSection(
`Audio${suffix}`,
"Transcript",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
continue;
}
if (output.kind === "image.description") {
sections.push(
formatSection(
`Image${suffix}`,
"Description",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
continue;
}
sections.push(
formatSection(
`Video${suffix}`,
"Description",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
}
return sections.join("\n\n").trim();
}
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
if (outputs.length === 1) {
return outputs[0].text;
}
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}

View File

@@ -0,0 +1,10 @@
export * from "./active-model.js";
export * from "./defaults.js";
export * from "./errors.js";
export * from "./format.js";
export * from "./openai-compatible-video.js";
export * from "./output-extract.js";
export * from "./provider-id.js";
export * from "./provider-supports.js";
export * from "./types.js";
export * from "./video.js";

View File

@@ -0,0 +1,66 @@
export type OpenAiCompatibleVideoPayload = {
choices?: Array<{
message?: {
content?: string | Array<{ text?: string }>;
reasoning_content?: string;
};
}>;
};
export function resolveMediaUnderstandingString(
value: string | undefined,
fallback: string,
): string {
const trimmed = value?.trim();
return trimmed || fallback;
}
export function coerceOpenAiCompatibleVideoText(
payload: OpenAiCompatibleVideoPayload,
): string | null {
const message = payload.choices?.[0]?.message;
if (!message) {
return null;
}
if (typeof message.content === "string" && message.content.trim()) {
return message.content.trim();
}
if (Array.isArray(message.content)) {
const text = message.content
.map((part) => part.text?.trim() ?? "")
.filter(Boolean)
.join("\n");
if (text) {
return text;
}
}
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) {
return message.reasoning_content.trim();
}
return null;
}
export function buildOpenAiCompatibleVideoRequestBody(params: {
model: string;
prompt: string;
mime: string;
buffer: Buffer;
}) {
return {
model: params.model,
messages: [
{
role: "user",
content: [
{ type: "text", text: params.prompt },
{
type: "video_url",
video_url: {
url: `data:${params.mime};base64,${params.buffer.toString("base64")}`,
},
},
],
},
],
};
}

View File

@@ -0,0 +1,26 @@
function extractLastJsonObject(raw: string): unknown {
const trimmed = raw.trim();
const start = trimmed.lastIndexOf("{");
if (start === -1) {
return null;
}
const slice = trimmed.slice(start);
try {
return JSON.parse(slice);
} catch {
return null;
}
}
export function extractGeminiResponse(raw: string): string | null {
const payload = extractLastJsonObject(raw);
if (!payload || typeof payload !== "object") {
return null;
}
const response = (payload as { response?: unknown }).response;
if (typeof response !== "string") {
return null;
}
const trimmed = response.trim();
return trimmed || null;
}

View File

@@ -0,0 +1,25 @@
function normalizeProviderId(provider: string): string {
return provider.trim().toLowerCase();
}
export function normalizeMediaProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") {
return "google";
}
if (normalized === "minimax-cn") {
return "minimax";
}
if (normalized === "minimax-portal-cn") {
return "minimax-portal";
}
return normalized;
}
export function normalizeMediaExecutionProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") {
return normalized;
}
return normalizeMediaProviderId(normalized);
}

View File

@@ -0,0 +1,17 @@
import type { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.js";
export function providerSupportsCapability(
provider: MediaUnderstandingProvider | undefined,
capability: MediaUnderstandingCapability,
): boolean {
if (!provider) {
return false;
}
if (capability === "audio") {
return Boolean(provider.transcribeAudio);
}
if (capability === "image") {
return Boolean(provider.describeImage);
}
return Boolean(provider.describeVideo);
}

View File

@@ -0,0 +1,39 @@
export type MediaUnderstandingKind =
| "audio.transcription"
| "video.description"
| "image.description";
export type MediaUnderstandingCapability = "image" | "audio" | "video";
export type MediaUnderstandingCapabilityRegistry = Map<
string,
{
capabilities?: MediaUnderstandingCapability[];
}
>;
export type MediaAttachment = {
path?: string;
url?: string;
mime?: string;
index: number;
alreadyTranscribed?: boolean;
};
export type MediaUnderstandingOutput = {
kind: MediaUnderstandingKind;
attachmentIndex: number;
text: string;
provider: string;
model?: string;
};
export type MediaUnderstandingProvider = {
id: string;
capabilities?: MediaUnderstandingCapability[];
transcribeAudio?: unknown;
describeVideo?: unknown;
describeImage?: unknown;
describeImages?: unknown;
extractStructured?: unknown;
};

View File

@@ -0,0 +1,10 @@
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
export function estimateBase64Size(bytes: number): number {
return Math.ceil(bytes / 3) * 4;
}
export function resolveVideoMaxBase64Bytes(maxBytes: number): number {
const expanded = Math.floor(maxBytes * (4 / 3));
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
}

302
pnpm-lock.yaml generated
View File

@@ -304,9 +304,6 @@ importers:
specifier: 4.1.7
version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.3)(yaml@2.9.0))
optionalDependencies:
sharp:
specifier: 0.34.5
version: 0.34.5
sqlite-vec:
specifier: 0.1.9
version: 0.1.9
@@ -1694,7 +1691,7 @@ importers:
version: 2.2.3
baileys:
specifier: 7.0.0-rc13
version: 7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5)
version: 7.0.0-rc13(audio-decode@2.2.3)
typebox:
specifier: 1.1.38
version: 1.1.38
@@ -1791,6 +1788,12 @@ importers:
specifier: 2.9.0
version: 2.9.0
packages/code-mode-runtime:
dependencies:
quickjs-wasi:
specifier: 3.0.0
version: 3.0.0
packages/gateway-client:
dependencies:
'@openclaw/gateway-protocol':
@@ -1832,6 +1835,8 @@ importers:
packages/media-generation-core: {}
packages/media-understanding-common: {}
packages/memory-host-sdk: {}
packages/net-policy:
@@ -2734,159 +2739,6 @@ packages:
peerDependencies:
hono: 4.12.18
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -6598,10 +6450,6 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -8358,103 +8206,6 @@ snapshots:
dependencies:
hono: 4.12.18
'@img/colour@1.1.0':
optional: true
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.3
@@ -10105,7 +9856,7 @@ snapshots:
bail@2.0.2: {}
baileys@7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5):
baileys@7.0.0-rc13(audio-decode@2.2.3):
dependencies:
'@cacheable/node-cache': 1.7.6
'@hapi/boom': 9.1.4
@@ -10120,7 +9871,6 @@ snapshots:
ws: 8.21.0
optionalDependencies:
audio-decode: 2.2.3
sharp: 0.34.5
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -12556,38 +12306,6 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.0
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
optional: true
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0

View File

@@ -106,7 +106,6 @@ allowBuilds:
koffi: false
node-llama-cpp: true
protobufjs: true
sharp: true
tree-sitter-bash: false
openclaw: true
"@openclaw/proxyline": true

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