mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
Reduce packaged app size (#1026)
This commit is contained in:
@@ -25,7 +25,7 @@ import { listAgentsSnapshot } from '../../utils/agent-config';
|
||||
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
|
||||
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
|
||||
const OPENAI_OAUTH_RUNTIME_PROVIDER = 'openai-codex';
|
||||
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
|
||||
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.5`;
|
||||
|
||||
/**
|
||||
* Provider types that are not in the built-in provider registry (no `providerConfig.api`).
|
||||
|
||||
@@ -12,7 +12,7 @@ export type BrowserOAuthProviderType = 'google' | 'openai';
|
||||
const GOOGLE_RUNTIME_PROVIDER_ID = 'google-gemini-cli';
|
||||
const GOOGLE_OAUTH_DEFAULT_MODEL = 'gemini-3-pro-preview';
|
||||
const OPENAI_RUNTIME_PROVIDER_ID = 'openai-codex';
|
||||
const OPENAI_OAUTH_DEFAULT_MODEL = 'gpt-5.4';
|
||||
const OPENAI_OAUTH_DEFAULT_MODEL = 'gpt-5.5';
|
||||
|
||||
class BrowserOAuthManager extends EventEmitter {
|
||||
private activeProvider: BrowserOAuthProviderType | null = null;
|
||||
|
||||
@@ -517,8 +517,6 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
|
||||
enabled: true,
|
||||
entries: {
|
||||
[feishuPluginId]: { enabled: true },
|
||||
// Disable the built-in feishu plugin when using openclaw-lark
|
||||
...(feishuPluginId !== 'feishu' ? { feishu: { enabled: false } } : {}),
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -539,15 +537,10 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
|
||||
if (!currentConfig.plugins.entries) {
|
||||
currentConfig.plugins.entries = {};
|
||||
}
|
||||
// Remove conflicting feishu plugin entries; keep only the resolved plugin id.
|
||||
// When the resolved plugin id is NOT 'feishu', explicitly disable the
|
||||
// built-in feishu plugin (OpenClaw ships one in dist/extensions/feishu/)
|
||||
// to prevent it from conflicting with the official openclaw-lark plugin.
|
||||
if (feishuPluginId !== 'feishu') {
|
||||
currentConfig.plugins.entries['feishu'] = { enabled: false };
|
||||
} else {
|
||||
delete currentConfig.plugins.entries['feishu'];
|
||||
}
|
||||
// Remove conflicting feishu plugin entries; keep only the resolved
|
||||
// external plugin id. A disabled plugins.entries.feishu record
|
||||
// blocks openclaw-lark in OpenClaw's gateway startup planner.
|
||||
delete currentConfig.plugins.entries['feishu'];
|
||||
for (const candidateId of FEISHU_PLUGIN_ID_CANDIDATES) {
|
||||
if (candidateId !== feishuPluginId) {
|
||||
delete currentConfig.plugins.entries[candidateId];
|
||||
|
||||
@@ -516,6 +516,7 @@ const BUNDLED_ALLOWLIST_PRESERVE_IDS = new Set([
|
||||
'browser',
|
||||
'acpx',
|
||||
'memory-core',
|
||||
'codex',
|
||||
]);
|
||||
const AUTH_PROFILE_PROVIDER_KEY_MAP: Record<string, string> = {
|
||||
'openai-codex': 'openai',
|
||||
@@ -2290,32 +2291,25 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
}
|
||||
|
||||
|
||||
// ── Disable built-in 'feishu' when official openclaw-lark plugin is active ──
|
||||
// OpenClaw ships a built-in 'feishu' extension in dist/extensions/feishu/
|
||||
// that conflicts with the official @larksuite/openclaw-lark plugin
|
||||
// (id: 'openclaw-lark'). When the feishu channel IS configured and the
|
||||
// canonical plugin is NOT the built-in 'feishu' itself, we must:
|
||||
// 1. Remove bare 'feishu' from plugins.allow
|
||||
// 2. Explicitly disable the built-in feishu extension
|
||||
// ── Remove legacy built-in 'feishu' registration ───────────────
|
||||
// ClawX bundles Feishu via the official @larksuite/openclaw-lark
|
||||
// plugin and removes the old built-in dist/extensions/feishu tree.
|
||||
// Keeping plugins.entries.feishu={enabled:false} looks harmless, but
|
||||
// OpenClaw's channel startup planner treats it as an explicit blocker
|
||||
// for the feishu channel owner and skips openclaw-lark at runtime.
|
||||
const allowArr2 = Array.isArray(pluginsObj.allow) ? pluginsObj.allow as string[] : [];
|
||||
if (isFeishuConfigured) {
|
||||
const hasCanonicalFeishu = allowArr2.includes(canonicalFeishuId) || !!pEntries[canonicalFeishuId];
|
||||
if (hasCanonicalFeishu && canonicalFeishuId !== 'feishu') {
|
||||
// Remove bare 'feishu' from plugins.allow
|
||||
const bareFeishuIdx = allowArr2.indexOf('feishu');
|
||||
if (bareFeishuIdx !== -1) {
|
||||
allowArr2.splice(bareFeishuIdx, 1);
|
||||
console.log('[sanitize] Removed bare "feishu" from plugins.allow (openclaw-lark plugin is configured)');
|
||||
modified = true;
|
||||
}
|
||||
// Explicitly disable the built-in feishu extension so it doesn't
|
||||
// conflict with the official openclaw-lark plugin at runtime.
|
||||
// Simply deleting the entry is NOT sufficient — the built-in
|
||||
// extension in dist/extensions/feishu/ (enabledByDefault: true) will
|
||||
// still load unless explicitly marked as disabled.
|
||||
if (!pEntries.feishu || (pEntries.feishu as Record<string, unknown>).enabled !== false) {
|
||||
pEntries.feishu = { enabled: false };
|
||||
console.log('[sanitize] Disabled built-in feishu plugin (openclaw-lark plugin is configured)');
|
||||
if (pEntries.feishu) {
|
||||
delete pEntries.feishu;
|
||||
console.log('[sanitize] Removed legacy plugins.entries.feishu (openclaw-lark plugin is configured)');
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
id: packaged-runtime-pruning-guards
|
||||
title: Packaged Runtime Pruning Guards
|
||||
type: ai-coding-rule
|
||||
appliesTo:
|
||||
- plugin-lifecycle-management
|
||||
requiredProfiles:
|
||||
- fast
|
||||
---
|
||||
|
||||
Packaged runtime cleanup must remove only files that are not needed by the
|
||||
target artifact.
|
||||
|
||||
For macOS universal builds, architecture pruning must preserve both x64 and
|
||||
arm64 native payloads for the same platform. This includes scoped optional
|
||||
packages such as `@openai/codex-darwin-x64` and
|
||||
`@openai/codex-darwin-arm64`, plus per-arch native prebuild directories such
|
||||
as `tree-sitter-bash/prebuilds/darwin-x64` and
|
||||
`tree-sitter-bash/prebuilds/darwin-arm64`.
|
||||
|
||||
Any change to `scripts/after-pack.cjs` or `scripts/bundle-openclaw.mjs` that
|
||||
adds native-package pruning or known-runtime-junk cleanup must include a unit
|
||||
test covering the target architecture behavior, including `arch: universal`
|
||||
when the rule can affect macOS packaged builds.
|
||||
@@ -36,6 +36,7 @@ requiredRules:
|
||||
- channel-plugin-migration-guards
|
||||
- capability-owner-resolution
|
||||
- active-config-guards
|
||||
- packaged-runtime-pruning-guards
|
||||
---
|
||||
|
||||
Plugin lifecycle management covers bundled and external plugins as one system with different source types. The core model has two layers:
|
||||
@@ -52,5 +53,6 @@ Lifecycle stages:
|
||||
- Validate: ClawX verifies package manifests, dependencies, capability config, ownership uniqueness, and startup requirements.
|
||||
- Activate: only resolved and validated capabilities enter final OpenClaw runtime config.
|
||||
- Recover: failed upgrades, stale registrations, conflicts, and removed channels converge to a single diagnosable state with rollback or cleanup paths.
|
||||
- Package: cleanup and pruning keep packaged artifacts small without deleting target runtime assets; macOS universal packages keep both x64 and arm64 native payloads.
|
||||
|
||||
First-stage priority is integration safety: single-owner capability resolution, active config guards, direct regression tests for migration cases, and explicit task specs for resolution, validation, and recovery work.
|
||||
|
||||
@@ -19,6 +19,7 @@ expectedUserBehavior:
|
||||
- When the pointer's runtimeFile points outside the sessions/ folder (the OPENCLAW_TRAJECTORY_DIR override), that off-disk runtime file is also unlinked so no orphan trajectory remains anywhere on disk.
|
||||
- The session entry is removed from sessions.json so OpenClaw sessions.list stops returning it.
|
||||
- The sidebar list, sessionLabels and sessionLastActivity for the deleted key are cleared in the renderer store.
|
||||
- Any pending optimistic user message cache for the deleted session key is cleared so a later history reload cannot resurrect deleted chat bubbles.
|
||||
- Token usage history reported by the Dashboard stops including the deleted session.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
@@ -31,6 +32,7 @@ acceptance:
|
||||
- IPC channel name session:delete and HTTP route POST /api/sessions/delete are unchanged in shape.
|
||||
- Both the IPC handler in electron/main/ipc-handlers.ts and the HTTP mirror in electron/api/routes/sessions.ts unlink the same set of files for a given session id, sharing electron/utils/session-files.ts so the disk contract cannot drift.
|
||||
- The handler tolerates ENOENT (file already gone) and still updates sessions.json so the sidebar stops listing the entry.
|
||||
- Renderer delete-session paths clear any in-memory pending optimistic user messages for the deleted key before subsequent history loads run.
|
||||
- agentId from the sessionKey is validated against /^[A-Za-z0-9][A-Za-z0-9_-]*$/ in both surfaces and any sessionFile resolved to a path outside the agent sessions/ directory is refused (defence-in-depth against a corrupt sessions.json).
|
||||
- Absolute-path detection accepts POSIX paths, Windows backslash paths (C:\...) and Windows forward-slash paths (C:/...) so the sweep works on every supported OS.
|
||||
- The sweep also unlinks <id>.trajectory.jsonl and <id>.trajectory-path.json sidecars produced by OpenClaw's runtime trajectory writer.
|
||||
@@ -68,4 +70,7 @@ Renderer surface is untouched: the confirm dialog in `Sidebar.tsx`, the
|
||||
`useChatStore.deleteSession` API, and the host-api/api-client boundary all
|
||||
continue to work without changes. The only user-visible effect is that
|
||||
deleted conversations no longer leave hidden `.deleted.jsonl` files behind
|
||||
and no longer contribute to Dashboard token-usage history.
|
||||
and no longer contribute to Dashboard token-usage history. Deletion also
|
||||
clears renderer-only pending optimistic user messages keyed by the deleted
|
||||
session so the UI cannot rehydrate a locally staged message after the
|
||||
transcript and sidebar entry have been removed.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: packaged-runtime-pruning-guards
|
||||
title: Guard packaged runtime pruning for universal builds
|
||||
scenario: plugin-lifecycle-management
|
||||
taskType: plugin-lifecycle
|
||||
intent: Keep packaged OpenClaw runtime cleanup size-conscious without deleting native payloads required by macOS universal artifacts.
|
||||
touchedAreas:
|
||||
- scripts/after-pack.cjs
|
||||
- scripts/bundle-openclaw.mjs
|
||||
- scripts/openclaw-bundle-config.mjs
|
||||
- tests/unit/after-pack-cleanup.test.ts
|
||||
- tests/unit/openclaw-bundle-config.test.ts
|
||||
- harness/specs/rules/packaged-runtime-pruning-guards.md
|
||||
- harness/specs/tasks/packaged-runtime-pruning-guards.md
|
||||
expectedUserBehavior:
|
||||
- Packaged OpenAI Codex support works in macOS arm64, x64 and universal artifacts.
|
||||
- Packaged tree-sitter-bash runtime loading keeps a usable native prebuild for every architecture in the target artifact.
|
||||
- Size cleanup still removes non-target platform packages and known runtime junk for single-architecture builds.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
requiredTests:
|
||||
- tests/unit/after-pack-cleanup.test.ts
|
||||
- tests/unit/openclaw-bundle-config.test.ts
|
||||
acceptance:
|
||||
- `cleanupNativePlatformPackages` keeps same-platform x64 and arm64 native packages when the electron-builder arch resolves to `universal`.
|
||||
- `cleanupNodeModulesRuntimeJunk` keeps same-platform x64 and arm64 `tree-sitter-bash/prebuilds` directories when the target arch is `universal`.
|
||||
- Non-target platforms are still pruned from scoped native packages and tree-sitter-bash prebuilds.
|
||||
- Extra bundled OpenClaw packages still include `@openclaw/codex`, and the bundle script still skips a duplicate nested `openclaw` package.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
|
||||
This task captures packaged-runtime cleanup invariants for OpenClaw extension
|
||||
and native payload bundling. The size optimization path may prune unused
|
||||
platform binaries, generated declarations, source maps and known non-runtime
|
||||
files, but it must not treat macOS `universal` as a literal architecture.
|
||||
|
||||
Universal macOS artifacts contain both x64 and arm64 slices. Cleanup helpers
|
||||
therefore keep same-platform x64 and arm64 packages/prebuilds while still
|
||||
removing Linux, Windows and other non-target platform payloads.
|
||||
+17
-16
@@ -77,27 +77,12 @@
|
||||
"postversion": "node scripts/post-version-push.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
"@types/diff": "^8.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"clawhub": "^0.5.0",
|
||||
"diff": "^9.0.0",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.8.3",
|
||||
"katex": "^0.16.45",
|
||||
"lru-cache": "^11.2.6",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"ms": "^2.1.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"posthog-node": "^5.28.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tar": "^6.2.1",
|
||||
"ws": "^8.19.0",
|
||||
"xlsx": "^0.18.5"
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@buape/carbon": "0.16.0",
|
||||
@@ -108,6 +93,8 @@
|
||||
"@homebridge/ciao": "^1.3.7",
|
||||
"@larksuite/openclaw-lark": "2026.5.12",
|
||||
"@larksuiteoapi/node-sdk": "^1.61.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@openclaw/codex": "2026.5.12",
|
||||
"@openclaw/discord": "2026.5.12",
|
||||
"@openclaw/qqbot": "2026.5.12",
|
||||
"@openclaw/whatsapp": "2026.5.12",
|
||||
@@ -124,11 +111,13 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
"@soimy/dingtalk": "^3.6.2",
|
||||
"@tencent-connect/qqbot-connector": "^1.1.0",
|
||||
"@tencent-weixin/openclaw-weixin": "^2.4.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/diff": "^8.0.0",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -140,8 +129,10 @@
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"acpx": "0.5.3",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"chokidar": "^5.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"diff": "^9.0.0",
|
||||
"discord-api-types": "^0.38.47",
|
||||
"electron": "^40.6.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
@@ -154,10 +145,15 @@
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"i18next": "^25.8.11",
|
||||
"jsdom": "^28.1.0",
|
||||
"katex": "^0.16.45",
|
||||
"lru-cache": "^11.2.6",
|
||||
"lucide-react": "^0.563.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mpg123-decoder": "^1.0.3",
|
||||
"ms": "^2.1.3",
|
||||
"openclaw": "2026.5.12",
|
||||
"opusscript": "^0.1.1",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"playwright-core": "1.59.1",
|
||||
"png2icons": "^2.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -167,13 +163,17 @@
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"silk-wasm": "^3.7.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar": "^6.2.1",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "8.1.0",
|
||||
"use-stick-to-bottom": "^1.1.3",
|
||||
@@ -181,6 +181,7 @@
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
"vitest": "^4.0.18",
|
||||
"xlsx": "^0.18.5",
|
||||
"zustand": "^5.0.11",
|
||||
"zx": "^8.8.5"
|
||||
},
|
||||
|
||||
Generated
+140
-45
@@ -11,69 +11,24 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.48
|
||||
version: 0.34.48
|
||||
'@types/diff':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
chokidar:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
clawhub:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
electron-store:
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.2
|
||||
electron-updater:
|
||||
specifier: ^6.8.3
|
||||
version: 6.8.3
|
||||
katex:
|
||||
specifier: ^0.16.45
|
||||
version: 0.16.45
|
||||
lru-cache:
|
||||
specifier: ^11.2.6
|
||||
version: 11.2.7
|
||||
monaco-editor:
|
||||
specifier: ^0.55.1
|
||||
version: 0.55.1
|
||||
ms:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
node-machine-id:
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12
|
||||
pdfjs-dist:
|
||||
specifier: ^5.7.284
|
||||
version: 5.7.284
|
||||
posthog-node:
|
||||
specifier: ^5.28.0
|
||||
version: 5.28.5
|
||||
rehype-katex:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
remark-frontmatter:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
remark-math:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
tar:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.20.0
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
devDependencies:
|
||||
'@buape/carbon':
|
||||
specifier: 0.16.0
|
||||
@@ -99,6 +54,12 @@ importers:
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.61.1
|
||||
version: 1.62.0
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@openclaw/codex':
|
||||
specifier: 2026.5.12
|
||||
version: 2026.5.12(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(openclaw@2026.5.12(encoding@0.1.13))
|
||||
'@openclaw/discord':
|
||||
specifier: 2026.5.12
|
||||
version: 2026.5.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.9.1)(openclaw@2026.5.12(encoding@0.1.13))
|
||||
@@ -147,6 +108,9 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.48
|
||||
version: 0.34.48
|
||||
'@soimy/dingtalk':
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2(openclaw@2026.5.12(encoding@0.1.13))
|
||||
@@ -162,6 +126,9 @@ importers:
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@types/diff':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
'@types/node':
|
||||
specifier: ^25.3.0
|
||||
version: 25.5.0
|
||||
@@ -195,12 +162,18 @@ importers:
|
||||
autoprefixer:
|
||||
specifier: ^10.4.24
|
||||
version: 10.4.27(postcss@8.5.8)
|
||||
chokidar:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
discord-api-types:
|
||||
specifier: ^0.38.47
|
||||
version: 0.38.47
|
||||
@@ -237,18 +210,33 @@ importers:
|
||||
jsdom:
|
||||
specifier: ^28.1.0
|
||||
version: 28.1.0(@noble/hashes@2.0.1)
|
||||
katex:
|
||||
specifier: ^0.16.45
|
||||
version: 0.16.45
|
||||
lru-cache:
|
||||
specifier: ^11.2.6
|
||||
version: 11.2.7
|
||||
lucide-react:
|
||||
specifier: ^0.563.0
|
||||
version: 0.563.0(react@19.2.4)
|
||||
monaco-editor:
|
||||
specifier: ^0.55.1
|
||||
version: 0.55.1
|
||||
mpg123-decoder:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
ms:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
openclaw:
|
||||
specifier: 2026.5.12
|
||||
version: 2026.5.12(encoding@0.1.13)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
pdfjs-dist:
|
||||
specifier: ^5.7.284
|
||||
version: 5.7.284
|
||||
playwright-core:
|
||||
specifier: 1.59.1
|
||||
version: 1.59.1
|
||||
@@ -276,9 +264,18 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^7.13.0
|
||||
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
rehype-katex:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
remark-frontmatter:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
remark-math:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -297,6 +294,9 @@ importers:
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||
tar:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
@@ -318,6 +318,9 @@ importers:
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.1.1(@types/node@25.5.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.9.0))
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
zustand:
|
||||
specifier: ^5.0.11
|
||||
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||
@@ -1674,6 +1677,55 @@ packages:
|
||||
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
'@openai/codex@0.130.0':
|
||||
resolution: {integrity: sha512-WGDj+RZ3TXWC/7MlwprgLWOqzpwatPIINPhP3IRzHA0ni+o3QZ4i4xrS2uWwGmHUJ395J5JHwoZAAZYyfJyz6w==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
'@openai/codex@0.130.0-darwin-arm64':
|
||||
resolution: {integrity: sha512-R9pkGC7kwC8yQ8el5hvBlmugQlcsG/pHMEFgZluu03X9fD2TezGxdq3KqRDRCZuMYl07ILamVEoqknuJ0cq7MA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.130.0-darwin-x64':
|
||||
resolution: {integrity: sha512-gJ+7J8djevgtdra+NgDAiQQPW+O3KTsgGfE3E5dpDfww3zS5OCeV0V2dhxqnJdlOjOSDw99o0P2LqBv19mhpRw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.130.0-linux-arm64':
|
||||
resolution: {integrity: sha512-tFtH0V9/hEI3d9y7zP92BXI9FM4Z3+STNQaOR52Czv18TRtCFUp7CbIUYaToopuq6UBfnE1VKr8RLhwT5FcbmA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.130.0-linux-x64':
|
||||
resolution: {integrity: sha512-3VcNlez99xdnEf+kB1IOpWv9fICYV9PiGj4sLCO4TCcShLnyxe+YBGa3poknkvXLnMG0qiN9SMnYS2FGrMxQcA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.130.0-win32-arm64':
|
||||
resolution: {integrity: sha512-vdpmiNp57L/arZabltLXn8TyEtNa7W1meOEkr+3R6W/8ZyBt++wuqz1Orv134OT2grrcFJsIVCAIPiqUxCvBkA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@openai/codex@0.130.0-win32-x64':
|
||||
resolution: {integrity: sha512-FzMznm7fr5/nbjZgOujZ9Y9AbdGm7ji1FOoWiY3U+srqauvZaTgn6o6aCheSL7kuymu7nTLOO/cAyWV6NuesqQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@openclaw/codex@2026.5.12':
|
||||
resolution: {integrity: sha512-T2kaIg4+vS8AE0Jr7HgLzHSdhlUvCg0VbNqBto3g7frATj05gG/nX6gx2UEj4Y5f8DEKXHrLKwc+vyywVQemgw==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.5.12'
|
||||
peerDependenciesMeta:
|
||||
openclaw:
|
||||
optional: true
|
||||
|
||||
'@openclaw/discord@2026.5.12':
|
||||
resolution: {integrity: sha512-nyB2wDpSyHcXbDwWnGQ+KjnPXcbAOj8MiLe8YS6IJbZUjcqjtObuUn5vh72Srcixupugu1PzNZT0WntO8J4n0A==}
|
||||
peerDependencies:
|
||||
@@ -8713,6 +8765,49 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
'@openai/codex@0.130.0':
|
||||
optionalDependencies:
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.130.0-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.130.0-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.130.0-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.130.0-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.130.0-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.130.0-win32-x64'
|
||||
|
||||
'@openai/codex@0.130.0-darwin-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.130.0-darwin-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.130.0-linux-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.130.0-linux-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.130.0-win32-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.130.0-win32-x64':
|
||||
optional: true
|
||||
|
||||
'@openclaw/codex@2026.5.12(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(openclaw@2026.5.12(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@earendil-works/pi-coding-agent': 0.74.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.0)(zod@4.4.3)
|
||||
'@openai/codex': 0.130.0
|
||||
ajv: 8.20.0
|
||||
ws: 8.20.0
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.5.12(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@openclaw/discord@2026.5.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.9.1)(openclaw@2026.5.12(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@discordjs/voice': 0.19.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.9.1)(opusscript@0.1.1)
|
||||
|
||||
+85
-3
@@ -155,6 +155,7 @@ const PLATFORM_NATIVE_SCOPES = {
|
||||
'@reflink': /^reflink-(darwin|linux|win32)-(x64|arm64|x64-gnu|x64-musl|arm64-gnu|arm64-musl|x64-msvc|arm64-msvc)/,
|
||||
'@node-llama-cpp': /^(mac|linux|win)-(arm64|x64|armv7l)(-metal|-cuda|-cuda-ext|-vulkan)?$/,
|
||||
'@esbuild': /^(darwin|linux|win32|android|freebsd|netbsd|openbsd|sunos|aix|openharmony)-(x64|arm64|arm|ia32|loong64|mips64el|ppc64|riscv64|s390x)/,
|
||||
'@openai': /^codex-(darwin|linux|win32)-(x64|arm64)$/,
|
||||
};
|
||||
|
||||
// Unscoped packages that follow a <name>-<platform>-<arch> convention.
|
||||
@@ -173,6 +174,13 @@ function baseArch(rawArch) {
|
||||
return dash > 0 ? rawArch.slice(0, dash) : rawArch;
|
||||
}
|
||||
|
||||
function matchesTargetArch(pkgArch, targetArch) {
|
||||
if (targetArch === 'universal') {
|
||||
return pkgArch === 'x64' || pkgArch === 'arm64' || pkgArch === 'universal';
|
||||
}
|
||||
return pkgArch === targetArch || pkgArch === 'universal';
|
||||
}
|
||||
|
||||
function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
||||
let removed = 0;
|
||||
|
||||
@@ -190,7 +198,7 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
||||
|
||||
const isMatch =
|
||||
pkgPlatform === platform &&
|
||||
(pkgArch === arch || pkgArch === 'universal');
|
||||
matchesTargetArch(pkgArch, arch);
|
||||
|
||||
if (!isMatch) {
|
||||
try {
|
||||
@@ -215,7 +223,7 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
||||
|
||||
const isMatch =
|
||||
pkgPlatform === platform &&
|
||||
(pkgArch === arch || pkgArch === 'universal');
|
||||
matchesTargetArch(pkgArch, arch);
|
||||
|
||||
if (!isMatch) {
|
||||
try {
|
||||
@@ -229,6 +237,75 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
||||
return removed;
|
||||
}
|
||||
|
||||
function cleanupNodeModulesRuntimeJunk(nodeModulesDir, platform, arch) {
|
||||
let removed = 0;
|
||||
|
||||
const nodeWavDir = join(nodeModulesDir, 'node-wav');
|
||||
for (const name of ['x.json', 'x.js', 'x.js~', 'file.wav']) {
|
||||
try {
|
||||
const target = join(nodeWavDir, name);
|
||||
if (existsSync(target)) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
removed++;
|
||||
}
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
const treeSitterBashDir = join(nodeModulesDir, 'tree-sitter-bash');
|
||||
const treeSitterSrc = join(treeSitterBashDir, 'src');
|
||||
for (const name of ['parser.c', 'scanner.c', 'grammar.json', 'tree_sitter']) {
|
||||
try {
|
||||
const target = join(treeSitterSrc, name);
|
||||
if (existsSync(target)) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
removed++;
|
||||
}
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
const prebuildsDir = join(treeSitterBashDir, 'prebuilds');
|
||||
if (existsSync(prebuildsDir)) {
|
||||
for (const entry of readdirSync(prebuildsDir)) {
|
||||
const [entryPlatform, ...entryArchParts] = entry.split('-');
|
||||
const entryArch = baseArch(entryArchParts.join('-'));
|
||||
if (entryPlatform === platform && matchesTargetArch(entryArch, arch)) continue;
|
||||
try {
|
||||
rmSync(join(prebuildsDir, entry), { recursive: true, force: true });
|
||||
removed++;
|
||||
} catch { /* */ }
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
function cleanupKnownRuntimeJunk(rootDir, platform, arch) {
|
||||
let removed = 0;
|
||||
const stack = [rootDir];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop();
|
||||
let entries;
|
||||
try { entries = readdirSync(normWin(dir), { withFileTypes: true }); } catch { continue; }
|
||||
|
||||
if (basename(dir) === 'node_modules') {
|
||||
removed += cleanupNodeModulesRuntimeJunk(dir, platform, arch);
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
stack.push(join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
exports.__test = {
|
||||
cleanupNativePlatformPackages,
|
||||
cleanupNodeModulesRuntimeJunk,
|
||||
};
|
||||
|
||||
// ── Broken module patcher ─────────────────────────────────────────────────────
|
||||
// Some bundled packages have transpiled CJS that sets `module.exports = exports.default`
|
||||
// without ever assigning `exports.default`, leaving module.exports === undefined.
|
||||
@@ -621,6 +698,10 @@ exports.default = async function afterPack(context) {
|
||||
const pluginNM = join(pluginDestDir, 'node_modules');
|
||||
cleanupUnnecessaryFiles(pluginDestDir);
|
||||
if (existsSync(pluginNM)) {
|
||||
const pluginJunkRemoved = cleanupKnownRuntimeJunk(pluginDestDir, platform, arch);
|
||||
if (pluginJunkRemoved > 0) {
|
||||
console.log(`[after-pack] ✅ ${pluginId}: removed ${pluginJunkRemoved} known runtime junk files/directories.`);
|
||||
}
|
||||
cleanupKoffi(pluginNM, platform, arch);
|
||||
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
||||
}
|
||||
@@ -761,7 +842,8 @@ exports.default = async function afterPack(context) {
|
||||
// 2. General cleanup on the full openclaw directory (not just node_modules)
|
||||
console.log('[after-pack] 🧹 Cleaning up unnecessary files ...');
|
||||
const removedRoot = cleanupUnnecessaryFiles(openclawRoot);
|
||||
console.log(`[after-pack] ✅ Removed ${removedRoot} unnecessary files/directories.`);
|
||||
const removedKnownJunk = cleanupKnownRuntimeJunk(openclawRoot, platform, arch);
|
||||
console.log(`[after-pack] ✅ Removed ${removedRoot + removedKnownJunk} unnecessary files/directories.`);
|
||||
|
||||
// 3. Platform-specific: strip koffi non-target platform binaries
|
||||
const koffiRemoved = cleanupKoffi(dest, platform, arch);
|
||||
|
||||
@@ -177,6 +177,10 @@ echo` Virtual store root: ${openclawVirtualNM}`;
|
||||
queue.push({ nodeModulesDir: openclawVirtualNM, skipPkg: 'openclaw' });
|
||||
|
||||
const SKIP_PACKAGES = new Set([
|
||||
// Extra bundled extensions such as @openclaw/codex can declare openclaw as a
|
||||
// peer/optional dependency. The bundle already copies openclaw to OUTPUT root,
|
||||
// so do not also copy a duplicate into OUTPUT/node_modules/openclaw.
|
||||
'openclaw',
|
||||
'typescript',
|
||||
'@playwright/test',
|
||||
// @discordjs/opus is a native .node addon compiled for the system Node.js
|
||||
@@ -447,6 +451,25 @@ function patchBundledExtensionPackageJsons(extensionsRoot) {
|
||||
|
||||
patchBundledExtensionPackageJsons(extensionsDir);
|
||||
|
||||
function bundleExternalExtension(pkgName, extensionId) {
|
||||
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
|
||||
if (!fs.existsSync(pkgLink)) {
|
||||
throw new Error(`Missing extension package "${pkgName}". Run pnpm install first.`);
|
||||
}
|
||||
|
||||
const realPath = fs.realpathSync(pkgLink);
|
||||
const dest = path.join(extensionsDir, extensionId);
|
||||
fs.rmSync(normWin(dest), { recursive: true, force: true });
|
||||
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
|
||||
fs.cpSync(normWin(realPath), normWin(dest), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
});
|
||||
echo` Bundled external extension ${pkgName} -> dist/extensions/${extensionId}`;
|
||||
}
|
||||
|
||||
bundleExternalExtension('@openclaw/codex', 'codex');
|
||||
|
||||
// 6. Clean up the bundle to reduce package size
|
||||
//
|
||||
// This removes platform-agnostic waste: dev artifacts, docs, source maps,
|
||||
@@ -482,6 +505,47 @@ function rmSafe(target) {
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function cleanupNodeModulesRuntimeJunk(nodeModulesDir) {
|
||||
let removedCount = 0;
|
||||
|
||||
const nodeWavDir = path.join(nodeModulesDir, 'node-wav');
|
||||
for (const name of ['x.json', 'x.js', 'x.js~', 'file.wav']) {
|
||||
if (rmSafe(path.join(nodeWavDir, name))) removedCount++;
|
||||
}
|
||||
|
||||
// tree-sitter-bash ships C sources for rebuilding its native addon. Packaged
|
||||
// builds use the prebuilt addon/wasm; keep node-types.json because the CJS
|
||||
// entry exposes it as optional runtime metadata.
|
||||
const treeSitterSrc = path.join(nodeModulesDir, 'tree-sitter-bash', 'src');
|
||||
for (const name of ['parser.c', 'scanner.c', 'grammar.json', 'tree_sitter']) {
|
||||
if (rmSafe(path.join(treeSitterSrc, name))) removedCount++;
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
function cleanupKnownRuntimeJunk(rootDir) {
|
||||
let removedCount = 0;
|
||||
const stack = [rootDir];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop();
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
||||
|
||||
if (path.basename(dir) === 'node_modules') {
|
||||
removedCount += cleanupNodeModulesRuntimeJunk(dir);
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
stack.push(path.join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
function cleanupBundle(outputDir) {
|
||||
let removedCount = 0;
|
||||
const nm = path.join(outputDir, 'node_modules');
|
||||
@@ -620,6 +684,8 @@ function cleanupBundle(outputDir) {
|
||||
if (rmSafe(path.join(outputDir, rel))) removedCount++;
|
||||
}
|
||||
|
||||
removedCount += cleanupKnownRuntimeJunk(outputDir);
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ export const EXTRA_BUNDLED_PACKAGES = [
|
||||
// transitive dependency graph from the app bundle context.
|
||||
'playwright-core',
|
||||
|
||||
// The Codex agent harness is published as an OpenClaw extension package.
|
||||
// Bundle it with the runtime so openai-codex models can register the codex
|
||||
// harness in packaged builds.
|
||||
'@openclaw/codex',
|
||||
|
||||
// Electron main process QR login flows resolve these files from the
|
||||
// bundled OpenClaw runtime context in packaged builds.
|
||||
'qrcode-terminal',
|
||||
|
||||
+73
-6
@@ -81,6 +81,15 @@ const CHAT_EVENT_DEDUPE_TTL_MS = 30_000;
|
||||
const HISTORY_PAGE_SIZE = 200;
|
||||
const HISTORY_MAX_RENDERED_MESSAGES = 1_000;
|
||||
const _chatEventDedupe = new Map<string, number>();
|
||||
const OPTIMISTIC_USER_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
type PendingOptimisticUserMessage = {
|
||||
message: RawMessage;
|
||||
timestampMs: number;
|
||||
createdAtMs: number;
|
||||
};
|
||||
|
||||
const _pendingOptimisticUserMessages = new Map<string, PendingOptimisticUserMessage[]>();
|
||||
|
||||
type SessionLabelSummary = {
|
||||
sessionKey: string;
|
||||
@@ -529,6 +538,62 @@ function matchesOptimisticUserMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
function rememberPendingOptimisticUserMessage(sessionKey: string, message: RawMessage, timestampMs: number): void {
|
||||
const now = Date.now();
|
||||
const existing = (_pendingOptimisticUserMessages.get(sessionKey) || [])
|
||||
.filter((entry) => now - entry.createdAtMs <= OPTIMISTIC_USER_MESSAGE_TTL_MS);
|
||||
existing.push({ message, timestampMs, createdAtMs: now });
|
||||
_pendingOptimisticUserMessages.set(sessionKey, existing);
|
||||
}
|
||||
|
||||
function clearPendingOptimisticUserMessages(sessionKey: string): void {
|
||||
_pendingOptimisticUserMessages.delete(sessionKey);
|
||||
}
|
||||
|
||||
function mergePendingOptimisticUserMessages(sessionKey: string, loadedMessages: RawMessage[]): RawMessage[] {
|
||||
const pending = _pendingOptimisticUserMessages.get(sessionKey);
|
||||
if (!pending || pending.length === 0) return loadedMessages;
|
||||
|
||||
const now = Date.now();
|
||||
let merged = loadedMessages;
|
||||
const stillPending: PendingOptimisticUserMessage[] = [];
|
||||
|
||||
for (const entry of pending) {
|
||||
if (now - entry.createdAtMs > OPTIMISTIC_USER_MESSAGE_TTL_MS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasServerEcho = loadedMessages.some((message) =>
|
||||
matchesOptimisticUserMessage(message, entry.message, entry.timestampMs),
|
||||
);
|
||||
if (hasServerEcho) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const alreadyRendered = merged.some((message) =>
|
||||
message.id === entry.message.id || matchesOptimisticUserMessage(message, entry.message, entry.timestampMs),
|
||||
);
|
||||
if (!alreadyRendered) {
|
||||
const insertAt = merged.findIndex((message) =>
|
||||
typeof message.timestamp === 'number' && toMs(message.timestamp) > entry.timestampMs,
|
||||
);
|
||||
merged = insertAt === -1
|
||||
? [...merged, entry.message]
|
||||
: [...merged.slice(0, insertAt), entry.message, ...merged.slice(insertAt)];
|
||||
}
|
||||
|
||||
stillPending.push(entry);
|
||||
}
|
||||
|
||||
if (stillPending.length > 0) {
|
||||
_pendingOptimisticUserMessages.set(sessionKey, stillPending);
|
||||
} else {
|
||||
_pendingOptimisticUserMessages.delete(sessionKey);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function snapshotStreamingAssistantMessage(
|
||||
currentStream: RawMessage | null,
|
||||
existingMessages: RawMessage[],
|
||||
@@ -1900,6 +1965,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
deleteSession: async (key: string) => {
|
||||
clearCachedSessionHistory(key);
|
||||
clearSessionLabelHydrationTracking(key);
|
||||
clearPendingOptimisticUserMessages(key);
|
||||
// Hard-delete the session's JSONL transcript on disk.
|
||||
// The main process unlinks <id>.jsonl plus any leftover
|
||||
// <id>.deleted.jsonl and <id>.jsonl.reset.* siblings, then removes the
|
||||
@@ -2141,19 +2207,19 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
// Restore file attachments for user/assistant messages (from cache + text patterns)
|
||||
const enrichedMessages = enrichWithCachedImages(filteredMessages);
|
||||
|
||||
// Preserve the optimistic user message during an active send.
|
||||
// The Gateway may not include the user's message in chat.history
|
||||
// until the run completes, causing it to flash out of the UI.
|
||||
let finalMessages = enrichedMessages;
|
||||
// Preserve optimistic user messages independently from sending state.
|
||||
// Gateway phase=end can clear sending before chat.history has persisted
|
||||
// the user turn; without this, an early quiet reload briefly removes it.
|
||||
let finalMessages = mergePendingOptimisticUserMessages(currentSessionKey, enrichedMessages);
|
||||
const userMsgAt = get().lastUserMessageAt;
|
||||
if (get().sending && userMsgAt) {
|
||||
const userMsMs = toMs(userMsgAt);
|
||||
const optimistic = getLatestOptimisticUserMessage(get().messages, userMsMs);
|
||||
const hasMatchingUser = optimistic
|
||||
? enrichedMessages.some((message) => matchesOptimisticUserMessage(message, optimistic, userMsMs))
|
||||
? finalMessages.some((message) => matchesOptimisticUserMessage(message, optimistic, userMsMs))
|
||||
: false;
|
||||
if (optimistic && !hasMatchingUser) {
|
||||
finalMessages = [...enrichedMessages, optimistic];
|
||||
finalMessages = [...finalMessages, optimistic];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2487,6 +2553,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
filePath: a.stagedPath,
|
||||
})),
|
||||
};
|
||||
rememberPendingOptimisticUserMessage(currentSessionKey, userMsg, nowMs);
|
||||
set((s) => ({
|
||||
messages: [...s.messages, userMsg],
|
||||
sending: true,
|
||||
|
||||
@@ -28,6 +28,15 @@ let _errorRecoveryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// the sending state after abortRun clears it.
|
||||
let _lastAbortedRunId: string | null = null;
|
||||
const _blockedRunEvents = new Map<string, Record<string, unknown>[]>();
|
||||
const OPTIMISTIC_USER_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
type PendingOptimisticUserMessage = {
|
||||
message: RawMessage;
|
||||
timestampMs: number;
|
||||
createdAtMs: number;
|
||||
};
|
||||
|
||||
const _pendingOptimisticUserMessages = new Map<string, PendingOptimisticUserMessage[]>();
|
||||
|
||||
function clearErrorRecoveryTimer(): void {
|
||||
if (_errorRecoveryTimer) {
|
||||
@@ -198,6 +207,62 @@ function matchesOptimisticUserMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
function rememberPendingOptimisticUserMessage(sessionKey: string, message: RawMessage, timestampMs: number): void {
|
||||
const now = Date.now();
|
||||
const existing = (_pendingOptimisticUserMessages.get(sessionKey) || [])
|
||||
.filter((entry) => now - entry.createdAtMs <= OPTIMISTIC_USER_MESSAGE_TTL_MS);
|
||||
existing.push({ message, timestampMs, createdAtMs: now });
|
||||
_pendingOptimisticUserMessages.set(sessionKey, existing);
|
||||
}
|
||||
|
||||
function clearPendingOptimisticUserMessages(sessionKey: string): void {
|
||||
_pendingOptimisticUserMessages.delete(sessionKey);
|
||||
}
|
||||
|
||||
function mergePendingOptimisticUserMessages(sessionKey: string, loadedMessages: RawMessage[]): RawMessage[] {
|
||||
const pending = _pendingOptimisticUserMessages.get(sessionKey);
|
||||
if (!pending || pending.length === 0) return loadedMessages;
|
||||
|
||||
const now = Date.now();
|
||||
let merged = loadedMessages;
|
||||
const stillPending: PendingOptimisticUserMessage[] = [];
|
||||
|
||||
for (const entry of pending) {
|
||||
if (now - entry.createdAtMs > OPTIMISTIC_USER_MESSAGE_TTL_MS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasServerEcho = loadedMessages.some((message) =>
|
||||
matchesOptimisticUserMessage(message, entry.message, entry.timestampMs),
|
||||
);
|
||||
if (hasServerEcho) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const alreadyRendered = merged.some((message) =>
|
||||
message.id === entry.message.id || matchesOptimisticUserMessage(message, entry.message, entry.timestampMs),
|
||||
);
|
||||
if (!alreadyRendered) {
|
||||
const insertAt = merged.findIndex((message) =>
|
||||
typeof message.timestamp === 'number' && toMs(message.timestamp) > entry.timestampMs,
|
||||
);
|
||||
merged = insertAt === -1
|
||||
? [...merged, entry.message]
|
||||
: [...merged.slice(0, insertAt), entry.message, ...merged.slice(insertAt)];
|
||||
}
|
||||
|
||||
stillPending.push(entry);
|
||||
}
|
||||
|
||||
if (stillPending.length > 0) {
|
||||
_pendingOptimisticUserMessages.set(sessionKey, stillPending);
|
||||
} else {
|
||||
_pendingOptimisticUserMessages.delete(sessionKey);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function snapshotStreamingAssistantMessage(
|
||||
currentStream: RawMessage | null,
|
||||
existingMessages: RawMessage[],
|
||||
@@ -1235,6 +1300,9 @@ export {
|
||||
isToolOnlyMessage,
|
||||
normalizeStreamingMessage,
|
||||
matchesOptimisticUserMessage,
|
||||
rememberPendingOptimisticUserMessage,
|
||||
clearPendingOptimisticUserMessages,
|
||||
mergePendingOptimisticUserMessages,
|
||||
snapshotStreamingAssistantMessage,
|
||||
getLatestOptimisticUserMessage,
|
||||
setHistoryPollTimer,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
isToolResultRole,
|
||||
loadMissingPreviews,
|
||||
matchesOptimisticUserMessage,
|
||||
mergePendingOptimisticUserMessages,
|
||||
toMs,
|
||||
} from './helpers';
|
||||
import { buildCronSessionHistoryPath, isCronSessionKey } from './cron-session-utils';
|
||||
@@ -106,19 +107,19 @@ export function createHistoryActions(
|
||||
// Restore file attachments for user/assistant messages (from cache + text patterns)
|
||||
const enrichedMessages = enrichWithCachedImages(filteredMessages);
|
||||
|
||||
// Preserve the optimistic user message during an active send.
|
||||
// The Gateway may not include the user's message in chat.history
|
||||
// until the run completes, causing it to flash out of the UI.
|
||||
let finalMessages = enrichedMessages;
|
||||
// Preserve optimistic user messages independently from sending state.
|
||||
// Gateway phase=end can clear sending before chat.history has persisted
|
||||
// the user turn; without this, an early quiet reload briefly removes it.
|
||||
let finalMessages = mergePendingOptimisticUserMessages(currentSessionKey, enrichedMessages);
|
||||
const userMsgAt = get().lastUserMessageAt;
|
||||
if (get().sending && userMsgAt) {
|
||||
const userMsMs = toMs(userMsgAt);
|
||||
const optimistic = getLatestOptimisticUserMessage(get().messages, userMsMs);
|
||||
const hasMatchingUser = optimistic
|
||||
? enrichedMessages.some((message) => matchesOptimisticUserMessage(message, optimistic, userMsMs))
|
||||
? finalMessages.some((message) => matchesOptimisticUserMessage(message, optimistic, userMsMs))
|
||||
: false;
|
||||
if (optimistic && !hasMatchingUser) {
|
||||
finalMessages = [...enrichedMessages, optimistic];
|
||||
finalMessages = [...finalMessages, optimistic];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
setHistoryPollTimer,
|
||||
setLastChatEventAt,
|
||||
setLastAbortedRunId,
|
||||
rememberPendingOptimisticUserMessage,
|
||||
takeBlockedRunEvents,
|
||||
upsertImageCacheEntry,
|
||||
} from './helpers';
|
||||
@@ -103,6 +104,7 @@ export function createRuntimeSendActions(set: ChatSet, get: ChatGet): Pick<Runti
|
||||
source: 'user-upload',
|
||||
})),
|
||||
};
|
||||
rememberPendingOptimisticUserMessage(currentSessionKey, userMsg, nowMs);
|
||||
set((s) => ({
|
||||
messages: [...s.messages, userMsg],
|
||||
sending: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers';
|
||||
import { clearPendingOptimisticUserMessages, getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers';
|
||||
import { DEFAULT_CANONICAL_PREFIX, DEFAULT_SESSION_KEY, type ChatSession, type RawMessage } from './types';
|
||||
import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api';
|
||||
|
||||
@@ -265,6 +265,7 @@ export function createSessionActions(
|
||||
|
||||
deleteSession: async (key: string) => {
|
||||
clearSessionLabelHydrationTracking(key);
|
||||
clearPendingOptimisticUserMessages(key);
|
||||
// Hard-delete the session's JSONL transcript on disk.
|
||||
// The main process unlinks <id>.jsonl plus any leftover
|
||||
// <id>.deleted.jsonl and <id>.jsonl.reset.* siblings, then removes the
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment node
|
||||
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
type AfterPackTestHooks = {
|
||||
cleanupNativePlatformPackages: (nodeModulesDir: string, platform: string, arch: string) => number;
|
||||
cleanupNodeModulesRuntimeJunk: (nodeModulesDir: string, platform: string, arch: string) => number;
|
||||
};
|
||||
|
||||
const afterPack = require('../../scripts/after-pack.cjs') as { __test?: AfterPackTestHooks };
|
||||
|
||||
describe('after-pack cleanup helpers', () => {
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeTempNodeModules(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'clawx-after-pack-'));
|
||||
tempRoots.push(root);
|
||||
const nodeModules = join(root, 'node_modules');
|
||||
mkdirSync(nodeModules, { recursive: true });
|
||||
return nodeModules;
|
||||
}
|
||||
|
||||
function makePackage(dir: string): void {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, 'package.json'), '{"version":"1.0.0"}\n', 'utf8');
|
||||
}
|
||||
|
||||
it('keeps both mac Codex native packages for universal builds', () => {
|
||||
const nodeModules = makeTempNodeModules();
|
||||
makePackage(join(nodeModules, '@openai', 'codex-darwin-arm64'));
|
||||
makePackage(join(nodeModules, '@openai', 'codex-darwin-x64'));
|
||||
makePackage(join(nodeModules, '@openai', 'codex-linux-x64'));
|
||||
|
||||
afterPack.__test!.cleanupNativePlatformPackages(nodeModules, 'darwin', 'universal');
|
||||
|
||||
expect(existsSync(join(nodeModules, '@openai', 'codex-darwin-arm64'))).toBe(true);
|
||||
expect(existsSync(join(nodeModules, '@openai', 'codex-darwin-x64'))).toBe(true);
|
||||
expect(existsSync(join(nodeModules, '@openai', 'codex-linux-x64'))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps both mac tree-sitter-bash prebuilds for universal builds', () => {
|
||||
const nodeModules = makeTempNodeModules();
|
||||
const prebuilds = join(nodeModules, 'tree-sitter-bash', 'prebuilds');
|
||||
mkdirSync(join(prebuilds, 'darwin-arm64'), { recursive: true });
|
||||
mkdirSync(join(prebuilds, 'darwin-x64'), { recursive: true });
|
||||
mkdirSync(join(prebuilds, 'linux-x64'), { recursive: true });
|
||||
|
||||
afterPack.__test!.cleanupNodeModulesRuntimeJunk(nodeModules, 'darwin', 'universal');
|
||||
|
||||
expect(existsSync(join(prebuilds, 'darwin-arm64'))).toBe(true);
|
||||
expect(existsSync(join(prebuilds, 'darwin-x64'))).toBe(true);
|
||||
expect(existsSync(join(prebuilds, 'linux-x64'))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe('WeCom plugin configuration', () => {
|
||||
expect(plugins.entries['wecom'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes feishu plugin registration to openclaw-lark and disables built-in feishu on save', async () => {
|
||||
it('normalizes feishu plugin registration to openclaw-lark and removes built-in feishu on save', async () => {
|
||||
const { saveChannelConfig, writeOpenClawConfig } = await import('@electron/utils/channel-config');
|
||||
|
||||
await writeOpenClawConfig({
|
||||
@@ -184,7 +184,7 @@ describe('WeCom plugin configuration', () => {
|
||||
expect(plugins.allow).not.toContain('feishu');
|
||||
expect(plugins.allow).not.toContain('feishu-openclaw-plugin');
|
||||
expect(plugins.entries['openclaw-lark']).toEqual({ enabled: true });
|
||||
expect(plugins.entries.feishu).toEqual({ enabled: false });
|
||||
expect(plugins.entries.feishu).toBeUndefined();
|
||||
expect(plugins.entries['feishu-openclaw-plugin']).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ vi.mock('@/stores/chat/helpers', () => ({
|
||||
isInternalMessage: (...args: unknown[]) => isInternalMessage(...args),
|
||||
isToolResultRole: (...args: unknown[]) => isToolResultRole(...args),
|
||||
loadMissingPreviews: (...args: unknown[]) => loadMissingPreviews(...args),
|
||||
mergePendingOptimisticUserMessages: (_sessionKey: string, messages: unknown[]) => messages,
|
||||
matchesOptimisticUserMessage: (
|
||||
candidate: { role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> },
|
||||
optimistic: { role: string; timestamp?: number; content?: unknown; _attachedFiles?: Array<{ filePath?: string; fileName?: string; mimeType?: string; fileSize?: number }> },
|
||||
|
||||
@@ -483,4 +483,133 @@ describe('useChatStore startup history retry', () => {
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('keeps the optimistic user message when completion refresh wins the transcript write race', async () => {
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
let historyMessages: Array<Record<string, unknown>> = [];
|
||||
let resolveSend: ((value: { runId: string }) => void) | null = null;
|
||||
|
||||
gatewayRpcMock.mockImplementation((method: string) => {
|
||||
if (method === 'chat.send') {
|
||||
return new Promise((resolve) => {
|
||||
resolveSend = resolve as (value: { runId: string }) => void;
|
||||
});
|
||||
}
|
||||
if (method === 'chat.history') {
|
||||
return Promise.resolve({ messages: historyMessages });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
});
|
||||
|
||||
const sendPromise = useChatStore.getState().sendMessage('hello from app');
|
||||
expect(useChatStore.getState().messages.map((message) => message.content)).toEqual(['hello from app']);
|
||||
|
||||
// Simulate Gateway phase=end clearing send state before chat.history has
|
||||
// persisted the user turn.
|
||||
useChatStore.setState({
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadHistory(true);
|
||||
expect(useChatStore.getState().messages.map((message) => message.content)).toEqual(['hello from app']);
|
||||
|
||||
historyMessages = [{
|
||||
role: 'user',
|
||||
content: 'hello from app',
|
||||
timestamp: Date.now() / 1000,
|
||||
id: 'server-user',
|
||||
}];
|
||||
vi.advanceTimersByTime(1_000);
|
||||
await useChatStore.getState().loadHistory(true);
|
||||
|
||||
expect(useChatStore.getState().messages).toHaveLength(1);
|
||||
expect(useChatStore.getState().messages[0]).toMatchObject({
|
||||
id: 'server-user',
|
||||
role: 'user',
|
||||
content: 'hello from app',
|
||||
});
|
||||
|
||||
resolveSend?.({ runId: 'run-1' });
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
it('does not restore a pending optimistic message after deleting the session', async () => {
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
let resolveSend: ((value: { runId: string }) => void) | null = null;
|
||||
|
||||
gatewayRpcMock.mockImplementation((method: string) => {
|
||||
if (method === 'chat.send') {
|
||||
return new Promise((resolve) => {
|
||||
resolveSend = resolve as (value: { runId: string }) => void;
|
||||
});
|
||||
}
|
||||
if (method === 'chat.history') {
|
||||
return Promise.resolve({ messages: [] });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
hostApiFetchMock.mockImplementation((url: string) => {
|
||||
if (url === '/api/sessions/delete') {
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
return Promise.resolve({ messages: [] });
|
||||
});
|
||||
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
});
|
||||
|
||||
const sendPromise = useChatStore.getState().sendMessage('message that will be deleted');
|
||||
expect(useChatStore.getState().messages.map((message) => message.content)).toEqual([
|
||||
'message that will be deleted',
|
||||
]);
|
||||
|
||||
await useChatStore.getState().deleteSession('agent:main:main');
|
||||
expect(useChatStore.getState().messages).toEqual([]);
|
||||
|
||||
await useChatStore.getState().loadHistory(true);
|
||||
expect(useChatStore.getState().messages).toEqual([]);
|
||||
|
||||
resolveSend?.({ runId: 'run-deleted' });
|
||||
await sendPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,7 +465,7 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
expect(telegram.botToken).toBe('telegram-token');
|
||||
});
|
||||
|
||||
it('normalizes legacy feishu plugin state to a single external plugin and disables built-in feishu', async () => {
|
||||
it('normalizes legacy feishu plugin state to a single external plugin and removes built-in feishu', async () => {
|
||||
await writeOpenClawJson({
|
||||
channels: {
|
||||
feishu: {
|
||||
@@ -507,7 +507,7 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
enabled: true,
|
||||
config: { preserved: true },
|
||||
});
|
||||
expect(entries.feishu).toEqual({ enabled: false });
|
||||
expect(entries.feishu).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes residual feishu plugin registrations when feishu channel is not configured', async () => {
|
||||
@@ -738,6 +738,7 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
{ dir: 'groq', id: 'groq', enabledByDefault: true },
|
||||
{ dir: 'alibaba', id: 'alibaba', enabledByDefault: true },
|
||||
{ dir: 'memory-core', id: 'memory-core' },
|
||||
{ dir: 'codex', id: 'codex', providers: ['codex'] },
|
||||
{ dir: 'openrouter', id: 'openrouter', enabledByDefault: true, providers: ['openrouter'] },
|
||||
{ dir: 'anthropic', id: 'anthropic', enabledByDefault: true, providers: ['anthropic'] },
|
||||
]) {
|
||||
@@ -780,6 +781,7 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
expect(allow).toContain('custom-plugin');
|
||||
expect(allow).toContain('browser');
|
||||
expect(allow).toContain('memory-core');
|
||||
expect(allow).toContain('codex');
|
||||
expect(allow).toContain('alibaba');
|
||||
expect(allow).not.toContain('groq');
|
||||
expect(allow).toContain('openrouter');
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('openclaw bundle config', () => {
|
||||
'silk-wasm',
|
||||
'acpx',
|
||||
'playwright-core',
|
||||
'@openclaw/codex',
|
||||
'qrcode-terminal',
|
||||
]));
|
||||
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as {
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
expect(gateway.debouncedRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses gpt-5.4 as the browser OAuth default model for OpenAI', async () => {
|
||||
it('uses gpt-5.5 as the browser OAuth default model for OpenAI', async () => {
|
||||
mocks.getProvider.mockResolvedValue(
|
||||
createProvider({
|
||||
id: 'openai-personal',
|
||||
@@ -209,7 +209,7 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
|
||||
expect(mocks.setOpenClawDefaultModel).toHaveBeenCalledWith(
|
||||
'openai-codex',
|
||||
'openai-codex/gpt-5.4',
|
||||
'openai-codex/gpt-5.5',
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ async function sanitizeConfig(
|
||||
'qqbot',
|
||||
]);
|
||||
const BUNDLED_ALLOWLIST_PRESERVE_IDS = new Set(
|
||||
bundledPlugins?.preserveIds ?? ['browser', 'acpx', 'memory-core'],
|
||||
bundledPlugins?.preserveIds ?? ['browser', 'acpx', 'memory-core', 'codex'],
|
||||
);
|
||||
|
||||
/** Non-throwing async existence check. */
|
||||
@@ -852,7 +852,7 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
});
|
||||
|
||||
const bundled = {
|
||||
all: ['browser', 'acpx', 'memory-core', 'openai', 'anthropic', 'diffs'],
|
||||
all: ['browser', 'acpx', 'memory-core', 'codex', 'openai', 'anthropic', 'diffs'],
|
||||
enabledByDefault: ['browser', 'acpx', 'openai', 'anthropic'],
|
||||
providersByPluginId: {
|
||||
openai: ['openai'],
|
||||
@@ -866,7 +866,7 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
const result = await readConfig();
|
||||
const plugins = result.plugins as Record<string, unknown>;
|
||||
const allow = plugins.allow as string[];
|
||||
expect(allow).toEqual(expect.arrayContaining(['customPlugin', 'browser', 'acpx', 'memory-core']));
|
||||
expect(allow).toEqual(expect.arrayContaining(['customPlugin', 'browser', 'acpx', 'memory-core', 'codex']));
|
||||
expect(allow).not.toContain('openai');
|
||||
expect(allow).not.toContain('anthropic');
|
||||
expect(allow).not.toContain('diffs');
|
||||
|
||||
Reference in New Issue
Block a user