From 955b0256976a7f964db8ce968ac325594dbf3c15 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 13:07:03 +0100 Subject: [PATCH] feat: add native sqlite Kysely dialect Add an owned Kysely dialect for native node:sqlite, raise the Node 22 floor to 22.16+ for StatementSync.columns(), and cover select/returning/stale insert id behavior. --- CHANGELOG.md | 1 + README.md | 4 +- SECURITY.md | 4 +- config/knip.config.ts | 1 + docs/index.md | 2 +- docs/install/ansible.md | 2 +- docs/install/bun.md | 2 +- docs/install/index.md | 2 +- docs/install/installer.md | 4 +- docs/install/node.md | 4 +- docs/platforms/linux.md | 2 +- docs/platforms/mac/bundled-gateway.md | 2 +- docs/platforms/mac/dev-setup.md | 2 +- docs/platforms/mac/signing.md | 2 +- docs/start/getting-started.md | 2 +- docs/start/setup.md | 2 +- .../src/memory/provider-adapters.ts | 2 +- package.json | 3 +- pnpm-lock.yaml | 9 + scripts/lib/dependency-ownership.json | 5 + src/cli/update-cli.test.ts | 4 +- src/cli/update-cli/update-command.ts | 2 +- src/commands/doctor-gateway-services.ts | 2 +- src/daemon/program-args.ts | 2 +- src/daemon/runtime-paths.test.ts | 22 +- src/daemon/runtime-paths.ts | 2 +- src/daemon/service-audit.ts | 2 +- src/daemon/service-env.test.ts | 4 +- src/infra/kysely-node-sqlite.test.ts | 63 ++++++ src/infra/kysely-node-sqlite.ts | 212 ++++++++++++++++++ src/infra/runtime-guard.test.ts | 32 +-- src/infra/runtime-guard.ts | 4 +- src/infra/update-check.test.ts | 4 +- 33 files changed, 352 insertions(+), 60 deletions(-) create mode 100644 src/infra/kysely-node-sqlite.test.ts create mode 100644 src/infra/kysely-node-sqlite.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index df13c13dc507..068f902b31be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Runtime/install: raise the supported Node 22 floor to `22.16+` so native SQLite query handling can rely on the `node:sqlite` statement metadata API while continuing to recommend Node 24. (#78921) - Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner. - Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics. - Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana. diff --git a/README.md b/README.md index 3bee5fbba4d8..2dcc71e50ef9 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Model note: while many providers and models are supported, prefer a current flag ## Install (recommended) -Runtime: **Node 24 (recommended) or Node 22.14+**. +Runtime: **Node 24 (recommended) or Node 22.16+**. ```bash npm install -g openclaw@latest @@ -109,7 +109,7 @@ OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so i ## Quick start (TL;DR) -Runtime: **Node 24 (recommended) or Node 22.14+**. +Runtime: **Node 24 (recommended) or Node 22.16+**. Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started) diff --git a/SECURITY.md b/SECURITY.md index 5cc0c44f8051..bbdb77819018 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -312,7 +312,7 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for * ### Node.js Version -OpenClaw requires **Node.js 22.14.0 or later** (LTS). This version includes important security patches: +OpenClaw requires **Node.js 22.16.0 or later** (LTS). This version includes important security patches: - CVE-2025-59466: async_hooks DoS vulnerability - CVE-2026-21636: Permission model bypass vulnerability @@ -320,7 +320,7 @@ OpenClaw requires **Node.js 22.14.0 or later** (LTS). This version includes impo Verify your Node.js version: ```bash -node --version # Should be v22.14.0 or later +node --version # Should be v22.16.0 or later ``` ### Docker Security diff --git a/config/knip.config.ts b/config/knip.config.ts index 6724a5fbb44a..59211669c1f2 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -9,6 +9,7 @@ const rootEntries = [ "src/index.ts!", "src/entry.ts!", "src/cli/daemon-cli.ts!", + "src/infra/kysely-node-sqlite.ts!", "src/infra/warning-filter.ts!", "src/infra/command-explainer/index.ts!", bundledPluginFile("telegram", "src/audit.ts", "!"), diff --git a/docs/index.md b/docs/index.md index 58c5e564577d..261d30d6cb2a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -54,7 +54,7 @@ OpenClaw is a **self-hosted gateway** that connects your favorite chat apps and - **Agent-native**: built for coding agents with tool use, sessions, memory, and multi-agent routing - **Open source**: MIT licensed, community-driven -**What do you need?** Node 24 (recommended), or Node 22 LTS (`22.14+`) for compatibility, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available. +**What do you need?** Node 24 (recommended), or Node 22 LTS (`22.16+`) for compatibility, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available. ## How it works diff --git a/docs/install/ansible.md b/docs/install/ansible.md index aac86248a3b9..c076635042a8 100644 --- a/docs/install/ansible.md +++ b/docs/install/ansible.md @@ -46,7 +46,7 @@ The Ansible playbook installs and configures: 1. **Tailscale** -- mesh VPN for secure remote access 2. **UFW firewall** -- SSH + Tailscale ports only 3. **Docker CE + Compose V2** -- for the default agent sandbox backend -4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.14+`, remains supported) +4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.16+`, remains supported) 5. **OpenClaw** -- host-based, not containerized 6. **Systemd service** -- auto-start with security hardening diff --git a/docs/install/bun.md b/docs/install/bun.md index e76eb0d10f0b..7c8ca8f6a109 100644 --- a/docs/install/bun.md +++ b/docs/install/bun.md @@ -39,7 +39,7 @@ Bun is an optional local runtime for running TypeScript directly (`bun run ...`, Bun blocks dependency lifecycle scripts unless explicitly trusted. For this repo, the commonly blocked scripts are not required: -- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.14+`) +- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`) - `protobufjs` `postinstall` -- emits warnings about incompatible version schemes (no build artifacts) If you hit a runtime issue that requires these scripts, trust them explicitly: diff --git a/docs/install/index.md b/docs/install/index.md index 1f5dc7c17525..5ee0e12fbb7d 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -9,7 +9,7 @@ title: "Install" ## System requirements -- **Node 24** (recommended) or Node 22.14+ - the installer script handles this automatically +- **Node 24** (recommended) or Node 22.16+ - the installer script handles this automatically - **macOS, Linux, or Windows** - both native Windows and WSL2 are supported; WSL2 is more stable. See [Windows](/platforms/windows). - `pnpm` is only needed if you build from source diff --git a/docs/install/installer.md b/docs/install/installer.md index 54eeb1b877c0..9db280577309 100644 --- a/docs/install/installer.md +++ b/docs/install/installer.md @@ -71,7 +71,7 @@ Recommended for most interactive installs on macOS/Linux/WSL. Supports macOS and Linux (including WSL). If macOS is detected, installs Homebrew if missing. - Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.14+`, for compatibility. + Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.16+`, for compatibility. Installs Git if missing. @@ -284,7 +284,7 @@ by default, plus git-checkout installs under the same prefix flow. Requires PowerShell 5+. - If missing, attempts install via winget, then Chocolatey, then Scoop. Node 22 LTS, currently `22.14+`, remains supported for compatibility. + If missing, attempts install via winget, then Chocolatey, then Scoop. Node 22 LTS, currently `22.16+`, remains supported for compatibility. - `npm` method (default): global npm install using selected `-Tag`, launched from a writable installer temp directory so shells opened in protected folders such as `C:\` still work diff --git a/docs/install/node.md b/docs/install/node.md index 78870f976fab..78534f40cec1 100644 --- a/docs/install/node.md +++ b/docs/install/node.md @@ -7,7 +7,7 @@ read_when: - "npm install -g fails with permissions or PATH issues" --- -OpenClaw requires **Node 22.14 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically - this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs). +OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically - this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs). ## Check your version @@ -15,7 +15,7 @@ OpenClaw requires **Node 22.14 or newer**. **Node 24 is the default and recommen node -v ``` -If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.14.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. If Node isn't installed or the version is too old, pick an install method below. +If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.16.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. If Node isn't installed or the version is too old, pick an install method below. ## Install Node diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index d98674cfc2ec..f7b93698a141 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -14,7 +14,7 @@ Native Linux companion apps are planned. Contributions are welcome if you want t ## Beginner quick path (VPS) -1. Install Node 24 (recommended; Node 22 LTS, currently `22.14+`, still works for compatibility) +1. Install Node 24 (recommended; Node 22 LTS, currently `22.16+`, still works for compatibility) 2. `npm i -g openclaw@latest` 3. `openclaw onboard --install-daemon` 4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 @` diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index 2a9b58b64754..60ec30fc5bdb 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -14,7 +14,7 @@ running (or attaches to an existing local Gateway if one is already running). ## Install the CLI (required for local mode) -Node 24 is the default runtime on the Mac. Node 22 LTS, currently `22.14+`, still works for compatibility. Then install `openclaw` globally: +Node 24 is the default runtime on the Mac. Node 22 LTS, currently `22.16+`, still works for compatibility. Then install `openclaw` globally: ```bash npm install -g openclaw@ diff --git a/docs/platforms/mac/dev-setup.md b/docs/platforms/mac/dev-setup.md index 046fce0dc4b0..e8589eae5b52 100644 --- a/docs/platforms/mac/dev-setup.md +++ b/docs/platforms/mac/dev-setup.md @@ -14,7 +14,7 @@ Build and run the OpenClaw macOS application from source. Before building the app, ensure you have the following installed: 1. **Xcode 26.2+**: Required for Swift development. -2. **Node.js 24 & pnpm**: Recommended for the gateway, CLI, and packaging scripts. Node 22 LTS, currently `22.14+`, remains supported for compatibility. +2. **Node.js 24 & pnpm**: Recommended for the gateway, CLI, and packaging scripts. Node 22 LTS, currently `22.16+`, remains supported for compatibility. ## 1. Install Dependencies diff --git a/docs/platforms/mac/signing.md b/docs/platforms/mac/signing.md index 4a2eb79c42f5..e22b3fc7f02e 100644 --- a/docs/platforms/mac/signing.md +++ b/docs/platforms/mac/signing.md @@ -14,7 +14,7 @@ This app is usually built from [`scripts/package-mac-app.sh`](https://github.com - calls [`scripts/codesign-mac-app.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/codesign-mac-app.sh) to sign the main binary and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [macOS permissions](/platforms/mac/permissions)). - uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds). - inject build metadata into Info.plist: `OpenClawBuildTimestamp` (UTC) and `OpenClawGitCommit` (short hash) so the About pane can show build, git, and debug/release channel. -- **Packaging defaults to Node 24**: the script runs TS builds and the Control UI build. Node 22 LTS, currently `22.14+`, remains supported for compatibility. +- **Packaging defaults to Node 24**: the script runs TS builds and the Control UI build. Node 22 LTS, currently `22.16+`, remains supported for compatibility. - reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing). - runs a Team ID audit after signing and fails if any Mach-O inside the app bundle is signed by a different Team ID. Set `SKIP_TEAM_ID_CHECK=1` to bypass. diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index dade1aa176ee..8632e2686acf 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -12,7 +12,7 @@ and a working chat session. ## What you need -- **Node.js** — Node 24 recommended (Node 22.14+ also supported) +- **Node.js** — Node 24 recommended (Node 22.16+ also supported) - **An API key** from a model provider (Anthropic, OpenAI, Google, etc.) — onboarding will prompt you diff --git a/docs/start/setup.md b/docs/start/setup.md index 1f66ca4c688a..7cf883bedbdc 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -21,7 +21,7 @@ Pick a setup workflow based on how often you want updates and whether you want t ## Prereqs (from source) -- Node 24 recommended (Node 22 LTS, currently `22.14+`, still supported) +- Node 24 recommended (Node 22 LTS, currently `22.16+`, still supported) - `pnpm` required for source checkouts. OpenClaw loads bundled plugins from the `extensions/*` pnpm workspace packages in dev mode, so root `npm install` does not prepare the full source tree. diff --git a/extensions/memory-core/src/memory/provider-adapters.ts b/extensions/memory-core/src/memory/provider-adapters.ts index 4dcfa5ce2c50..173a8b4e127c 100644 --- a/extensions/memory-core/src/memory/provider-adapters.ts +++ b/extensions/memory-core/src/memory/provider-adapters.ts @@ -55,7 +55,7 @@ function formatLocalSetupError(err: unknown): string { : undefined, missing && detail ? `Detail: ${detail}` : null, "To enable local embeddings:", - "1) Use Node 24 (recommended for installs/updates; Node 22 LTS, currently 22.14+, remains supported)", + "1) Use Node 24 (recommended for installs/updates; Node 22 LTS, currently 22.16+, remains supported)", missing ? `2) Install ${NODE_LLAMA_CPP_RUNTIME_PACKAGE} next to the OpenClaw package or source checkout` : null, diff --git a/package.json b/package.json index 70ddf634a0f2..cb0b53e65be7 100644 --- a/package.json +++ b/package.json @@ -1716,6 +1716,7 @@ "jiti": "^2.6.1", "json5": "^2.2.3", "jszip": "^3.10.1", + "kysely": "0.28.17", "linkedom": "^0.18.12", "markdown-it": "14.1.1", "minimatch": "10.2.5", @@ -1775,7 +1776,7 @@ "uuid": "14.0.0" }, "engines": { - "node": ">=22.14.0" + "node": ">=22.16.0" }, "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8", "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9df4b4fb67e..f623f590a970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,9 @@ importers: jszip: specifier: ^3.10.1 version: 3.10.1 + kysely: + specifier: 0.28.17 + version: 0.28.17 linkedom: specifier: ^0.18.12 version: 0.18.12 @@ -6017,6 +6020,10 @@ packages: koffi@2.16.1: resolution: {integrity: sha512-0Ie6CfD026dNfWSosDw9dPxPzO9Rlyo0N8m5r05S8YjytIpuilzMFDMY4IDy/8xQsTwpuVinhncD+S8n3bcYZQ==} + kysely@0.28.17: + resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} + engines: {node: '>=20.0.0'} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -13100,6 +13107,8 @@ snapshots: koffi@2.16.1: optional: true + kysely@0.28.17: {} + lie@3.3.0: dependencies: immediate: 3.0.6 diff --git a/scripts/lib/dependency-ownership.json b/scripts/lib/dependency-ownership.json index 30bff124a369..93c576ead0fb 100644 --- a/scripts/lib/dependency-ownership.json +++ b/scripts/lib/dependency-ownership.json @@ -126,6 +126,11 @@ "class": "core-runtime", "risk": ["archive-parser", "untrusted-files"] }, + "kysely": { + "owner": "core:sqlite-storage", + "class": "core-runtime", + "risk": ["database-query-builder"] + }, "linkedom": { "owner": "plugin:web-readability", "class": "plugin-runtime", diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 4d5ee95a8185..059fb78bf5fc 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -469,7 +469,7 @@ describe("update-cli", () => { vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({ target: "latest", version: "9999.0.0", - nodeEngine: ">=22.14.0", + nodeEngine: ">=22.16.0", }); vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag: "latest", @@ -1778,7 +1778,7 @@ describe("update-cli", () => { vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({ target: "latest", version: "2026.3.23-2", - nodeEngine: ">=22.14.0", + nodeEngine: ">=22.16.0", }); nodeVersionSatisfiesEngine.mockReturnValue(false); diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index d3965ac0eb45..34073d5f5d11 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -647,7 +647,7 @@ async function resolvePackageRuntimePreflightError(params: { return [ `Node ${process.versions.node ?? "unknown"} is too old for openclaw@${targetLabel}.`, `The requested package requires ${status.nodeEngine}.`, - "Upgrade Node to 22.14+ or Node 24, then rerun `openclaw update`.", + "Upgrade Node to 22.16+ or Node 24, then rerun `openclaw update`.", "Bare `npm i -g openclaw` can silently install an older compatible release.", "After upgrading Node, use `npm i -g openclaw@latest`.", ].join("\n"); diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 9144effb44bf..23e8e78eefbb 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -428,7 +428,7 @@ export async function maybeRepairGatewayServiceConfig( note(warning, "Gateway runtime"); } note( - "System Node 22 LTS (22.14+) or Node 24 not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", + "System Node 22 LTS (22.16+) or Node 24 not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", "Gateway runtime", ); } diff --git a/src/daemon/program-args.ts b/src/daemon/program-args.ts index dfc262a504db..f19c16ec5a76 100644 --- a/src/daemon/program-args.ts +++ b/src/daemon/program-args.ts @@ -175,7 +175,7 @@ async function resolveBinaryPath(binary: string): Promise { throw new Error("Bun not found in PATH. Install bun: https://bun.sh"); } throw new Error( - "Node not found in PATH. Install Node 24 (recommended) or Node 22 LTS (22.14+).", + "Node not found in PATH. Install Node 24 (recommended) or Node 22 LTS (22.16+).", ); } } diff --git a/src/daemon/runtime-paths.test.ts b/src/daemon/runtime-paths.test.ts index f41398afa589..9fc5dcc26088 100644 --- a/src/daemon/runtime-paths.test.ts +++ b/src/daemon/runtime-paths.test.ts @@ -179,7 +179,7 @@ describe("resolvePreferredNodePath", () => { const execFile = vi .fn() .mockResolvedValueOnce({ stdout: "18.0.0\n", stderr: "" }) // execPath too old - .mockResolvedValueOnce({ stdout: "22.14.0\n", stderr: "" }); // system node ok + .mockResolvedValueOnce({ stdout: "22.16.0\n", stderr: "" }); // system node ok const result = await resolvePreferredNodePath({ env: {}, @@ -196,7 +196,7 @@ describe("resolvePreferredNodePath", () => { it("ignores execPath when it is not node", async () => { mockNodePathPresent(darwinNode); - const execFile = vi.fn().mockResolvedValue({ stdout: "22.14.0\n", stderr: "" }); + const execFile = vi.fn().mockResolvedValue({ stdout: "22.16.0\n", stderr: "" }); const result = await resolvePreferredNodePath({ env: {}, @@ -216,8 +216,8 @@ describe("resolvePreferredNodePath", () => { it("uses system node when it meets the minimum version", async () => { mockNodePathPresent(darwinNode); - // Node 22.14.0+ is the minimum required version - const execFile = vi.fn().mockResolvedValue({ stdout: "22.14.0\n", stderr: "" }); + // Node 22.16.0+ is the minimum required version + const execFile = vi.fn().mockResolvedValue({ stdout: "22.16.0\n", stderr: "" }); const result = await resolvePreferredNodePath({ env: {}, @@ -234,8 +234,8 @@ describe("resolvePreferredNodePath", () => { it("skips system node when it is too old", async () => { mockNodePathPresent(darwinNode); - // Node 22.13.x is below minimum 22.14.0 - const execFile = vi.fn().mockResolvedValue({ stdout: "22.13.0\n", stderr: "" }); + // Node 22.15.x is below minimum 22.16.0 + const execFile = vi.fn().mockResolvedValue({ stdout: "22.15.0\n", stderr: "" }); const result = await resolvePreferredNodePath({ env: {}, @@ -291,7 +291,7 @@ describe("resolveStableNodePath", () => { it("resolves versioned node@22 formula to opt symlink", async () => { mockNodePathPresent("/opt/homebrew/opt/node@22/bin/node"); - const result = await resolveStableNodePath("/opt/homebrew/Cellar/node@22/22.14.0/bin/node"); + const result = await resolveStableNodePath("/opt/homebrew/Cellar/node@22/22.16.0/bin/node"); expect(result).toBe("/opt/homebrew/opt/node@22/bin/node"); }); @@ -341,8 +341,8 @@ describe("resolveSystemNodeInfo", () => { it("returns supported info when version is new enough", async () => { mockNodePathPresent(darwinNode); - // Node 22.14.0+ is the minimum required version - const execFile = vi.fn().mockResolvedValue({ stdout: "22.14.0\n", stderr: "" }); + // Node 22.16.0+ is the minimum required version + const execFile = vi.fn().mockResolvedValue({ stdout: "22.16.0\n", stderr: "" }); const result = await resolveSystemNodeInfo({ env: {}, @@ -352,7 +352,7 @@ describe("resolveSystemNodeInfo", () => { expect(result).toEqual({ path: darwinNode, - version: "22.14.0", + version: "22.16.0", supported: true, }); }); @@ -441,7 +441,7 @@ describe("resolveSystemNodeInfo", () => { "/Users/me/.fnm/node-22/bin/node", ); - expect(warning).toContain("below the required Node 22.14+"); + expect(warning).toContain("below the required Node 22.16+"); expect(warning).toContain(darwinNode); }); diff --git a/src/daemon/runtime-paths.ts b/src/daemon/runtime-paths.ts index cf56b387e5d4..8ca52e991b3f 100644 --- a/src/daemon/runtime-paths.ts +++ b/src/daemon/runtime-paths.ts @@ -187,7 +187,7 @@ export function renderSystemNodeWarning( } const versionLabel = systemNode.version ?? "unknown"; const selectedLabel = selectedNodePath ? ` Using ${selectedNodePath} for the daemon.` : ""; - return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22.14+.${selectedLabel} Install Node 24 (recommended) or Node 22 LTS from nodejs.org or Homebrew.`; + return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22.16+.${selectedLabel} Install Node 24 (recommended) or Node 22 LTS from nodejs.org or Homebrew.`; } export { resolveStableNodePath }; diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index 6a24acec47a5..6ce5f18102e9 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -508,7 +508,7 @@ async function auditGatewayRuntime( issues.push({ code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeSystemMissing, message: - "System Node 22 LTS (22.14+) or Node 24 not found; install it before migrating away from version managers.", + "System Node 22 LTS (22.16+) or Node 24 not found; install it before migrating away from version managers.", level: "recommended", }); } diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index b55ce4e6f4f4..885f91e5cb6b 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -704,11 +704,11 @@ describe("buildServiceEnvironment", () => { env: { HOME: "/Users/user", VOLTA_HOME: "/Users/user/.volta" }, port: 18789, platform: "darwin", - extraPathDirs: ["/opt/homebrew/Cellar/node/22.14.0/bin"], + extraPathDirs: ["/opt/homebrew/Cellar/node/22.16.0/bin"], }); expect(env.PATH).toBe( - "/opt/homebrew/Cellar/node/22.14.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", + "/opt/homebrew/Cellar/node/22.16.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", ); }); }); diff --git a/src/infra/kysely-node-sqlite.test.ts b/src/infra/kysely-node-sqlite.test.ts new file mode 100644 index 000000000000..49081bf5b5ca --- /dev/null +++ b/src/infra/kysely-node-sqlite.test.ts @@ -0,0 +1,63 @@ +import { DatabaseSync } from "node:sqlite"; +import { Kysely, sql, type Generated } from "kysely"; +import { afterEach, describe, expect, it } from "vitest"; +import { NodeSqliteKyselyDialect } from "./kysely-node-sqlite.js"; + +type TestDatabase = { + person: { + id: Generated; + name: string; + }; +}; + +describe("NodeSqliteKyselyDialect", () => { + let db: Kysely | undefined; + + afterEach(async () => { + await db?.destroy(); + db = undefined; + }); + + it("uses node:sqlite with raw row-returning queries and returning clauses", async () => { + db = new Kysely({ + dialect: new NodeSqliteKyselyDialect({ + database: new DatabaseSync(":memory:"), + }), + }); + + await db.schema + .createTable("person") + .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement()) + .addColumn("name", "text", (col) => col.notNull()) + .execute(); + + await db.insertInto("person").values({ name: "Ada" }).execute(); + + await expect(db.selectFrom("person").selectAll().execute()).resolves.toEqual([ + { id: 1, name: "Ada" }, + ]); + await expect(sql`select name from person where id = ${1}`.execute(db)).resolves.toMatchObject({ + rows: [{ name: "Ada" }], + }); + await expect( + db.insertInto("person").values({ name: "Grace" }).returning(["id", "name"]).execute(), + ).resolves.toEqual([{ id: 2, name: "Grace" }]); + await expect( + sql`insert into person (name) values ('Lin') returning *`.execute(db), + ).resolves.toMatchObject({ + rows: [{ id: 3, name: "Lin" }], + }); + + const ignoredInsert = await sql` + insert or ignore into person (id, name) values (${1}, ${"Ada Again"}) + `.execute(db); + expect(ignoredInsert.insertId).toBeUndefined(); + expect(ignoredInsert.numAffectedRows).toBe(0n); + + const update = await sql`update person set name = ${"Ada Lovelace"} where id = ${1}`.execute( + db, + ); + expect(update.insertId).toBeUndefined(); + expect(update.numAffectedRows).toBe(1n); + }); +}); diff --git a/src/infra/kysely-node-sqlite.ts b/src/infra/kysely-node-sqlite.ts new file mode 100644 index 000000000000..5bd41957210d --- /dev/null +++ b/src/infra/kysely-node-sqlite.ts @@ -0,0 +1,212 @@ +import type { DatabaseSync, SQLInputValue } from "node:sqlite"; +import type { + DatabaseConnection, + DatabaseIntrospector, + Dialect, + DialectAdapter, + Driver, + Kysely, + QueryCompiler, + QueryResult, + TransactionSettings, +} from "kysely"; +import { + CompiledQuery, + IdentifierNode, + RawNode, + SqliteAdapter, + SqliteIntrospector, + SqliteQueryCompiler, + createQueryId, +} from "kysely"; + +type MaybePromise = T | Promise; + +export type NodeSqliteKyselyDialectConfig = { + database: DatabaseSync | (() => MaybePromise); + onCreateConnection?: (connection: DatabaseConnection) => MaybePromise; + transactionMode?: "deferred" | "immediate" | "exclusive"; +}; + +export class NodeSqliteKyselyDialect implements Dialect { + readonly #config: NodeSqliteKyselyDialectConfig; + + constructor(config: NodeSqliteKyselyDialectConfig) { + this.#config = Object.freeze({ ...config }); + } + + createDriver(): Driver { + return new NodeSqliteKyselyDriver(this.#config); + } + + createQueryCompiler(): QueryCompiler { + return new SqliteQueryCompiler(); + } + + createAdapter(): DialectAdapter { + return new SqliteAdapter(); + } + + createIntrospector(db: Kysely): DatabaseIntrospector { + return new SqliteIntrospector(db); + } +} + +class NodeSqliteKyselyDriver implements Driver { + readonly #config: NodeSqliteKyselyDialectConfig; + readonly #mutex = new ConnectionMutex(); + + #db?: DatabaseSync; + #connection?: DatabaseConnection; + + constructor(config: NodeSqliteKyselyDialectConfig) { + this.#config = Object.freeze({ ...config }); + } + + async init(): Promise { + this.#db = + typeof this.#config.database === "function" + ? await this.#config.database() + : this.#config.database; + + this.#connection = new NodeSqliteKyselyConnection(this.#db); + await this.#config.onCreateConnection?.(this.#connection); + } + + async acquireConnection(): Promise { + await this.#mutex.lock(); + return this.#connection!; + } + + async beginTransaction( + connection: DatabaseConnection, + _settings: TransactionSettings, + ): Promise { + const mode = this.#config.transactionMode ?? "deferred"; + await connection.executeQuery(CompiledQuery.raw(`begin ${mode}`)); + } + + async commitTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("commit")); + } + + async rollbackTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("rollback")); + } + + async savepoint( + connection: DatabaseConnection, + savepointName: string, + compileQuery: QueryCompiler["compileQuery"], + ): Promise { + await connection.executeQuery( + compileQuery(createSavepointCommand("savepoint", savepointName), createQueryId()), + ); + } + + async rollbackToSavepoint( + connection: DatabaseConnection, + savepointName: string, + compileQuery: QueryCompiler["compileQuery"], + ): Promise { + await connection.executeQuery( + compileQuery(createSavepointCommand("rollback to", savepointName), createQueryId()), + ); + } + + async releaseSavepoint( + connection: DatabaseConnection, + savepointName: string, + compileQuery: QueryCompiler["compileQuery"], + ): Promise { + await connection.executeQuery( + compileQuery(createSavepointCommand("release", savepointName), createQueryId()), + ); + } + + async releaseConnection(): Promise { + this.#mutex.unlock(); + } + + async destroy(): Promise { + this.#db?.close(); + this.#db = undefined; + this.#connection = undefined; + } +} + +class NodeSqliteKyselyConnection implements DatabaseConnection { + readonly #db: DatabaseSync; + + constructor(db: DatabaseSync) { + this.#db = db; + } + + executeQuery(compiledQuery: CompiledQuery): Promise> { + const { sql, parameters } = compiledQuery; + const stmt = this.#db.prepare(sql); + const sqliteParameters = parameters as SQLInputValue[]; + + if (stmt.columns().length > 0) { + return Promise.resolve({ rows: stmt.all(...sqliteParameters) as O[] }); + } + + const { changes, lastInsertRowid } = stmt.run(...sqliteParameters); + const baseResult: QueryResult = { + numAffectedRows: BigInt(changes), + rows: [], + }; + if (isInsertStatement(sql) && changes > 0) { + return Promise.resolve({ + ...baseResult, + insertId: BigInt(lastInsertRowid), + }); + } + return Promise.resolve(baseResult); + } + + async *streamQuery( + compiledQuery: CompiledQuery, + _chunkSize?: number, + ): AsyncIterableIterator> { + const { sql, parameters } = compiledQuery; + const stmt = this.#db.prepare(sql); + + for (const row of stmt.iterate(...(parameters as SQLInputValue[]))) { + yield { rows: [row as O] }; + } + } +} + +function isInsertStatement(sql: string): boolean { + return sql.trimStart().toLowerCase().startsWith("insert"); +} + +function createSavepointCommand(command: string, savepointName: string): RawNode { + return RawNode.createWithChildren([ + RawNode.createWithSql(`${command} `), + IdentifierNode.create(savepointName), + ]); +} + +class ConnectionMutex { + #promise?: Promise; + #resolve?: () => void; + + async lock(): Promise { + while (this.#promise) { + await this.#promise; + } + + this.#promise = new Promise((resolve) => { + this.#resolve = resolve; + }); + } + + unlock(): void { + const resolve = this.#resolve; + this.#promise = undefined; + this.#resolve = undefined; + resolve?.(); + } +} diff --git a/src/infra/runtime-guard.test.ts b/src/infra/runtime-guard.test.ts index 5ab1984bddd1..99d4e938e815 100644 --- a/src/infra/runtime-guard.test.ts +++ b/src/infra/runtime-guard.test.ts @@ -15,21 +15,21 @@ describe("runtime-guard", () => { it("parses semver with or without leading v", () => { expect(parseSemver("v22.1.3")).toEqual({ major: 22, minor: 1, patch: 3 }); expect(parseSemver("1.3.0")).toEqual({ major: 1, minor: 3, patch: 0 }); - expect(parseSemver("22.14.0-beta.1")).toEqual({ major: 22, minor: 14, patch: 0 }); + expect(parseSemver("22.16.0-beta.1")).toEqual({ major: 22, minor: 16, patch: 0 }); expect(parseSemver("invalid")).toBeNull(); }); it("compares versions correctly", () => { - expect(isAtLeast({ major: 22, minor: 14, patch: 0 }, { major: 22, minor: 14, patch: 0 })).toBe( + expect(isAtLeast({ major: 22, minor: 16, patch: 0 }, { major: 22, minor: 16, patch: 0 })).toBe( true, ); - expect(isAtLeast({ major: 22, minor: 15, patch: 0 }, { major: 22, minor: 14, patch: 0 })).toBe( + expect(isAtLeast({ major: 22, minor: 17, patch: 0 }, { major: 22, minor: 16, patch: 0 })).toBe( true, ); - expect(isAtLeast({ major: 22, minor: 13, patch: 0 }, { major: 22, minor: 14, patch: 0 })).toBe( + expect(isAtLeast({ major: 22, minor: 15, patch: 0 }, { major: 22, minor: 16, patch: 0 })).toBe( false, ); - expect(isAtLeast({ major: 21, minor: 9, patch: 0 }, { major: 22, minor: 14, patch: 0 })).toBe( + expect(isAtLeast({ major: 21, minor: 9, patch: 0 }, { major: 22, minor: 16, patch: 0 })).toBe( false, ); }); @@ -37,11 +37,11 @@ describe("runtime-guard", () => { it("validates runtime thresholds", () => { const nodeOk: RuntimeDetails = { kind: "node", - version: "22.14.0", + version: "22.16.0", execPath: "/usr/bin/node", pathEnv: "/usr/bin", }; - const nodeOld: RuntimeDetails = { ...nodeOk, version: "22.13.0" }; + const nodeOld: RuntimeDetails = { ...nodeOk, version: "22.15.0" }; const nodeTooOld: RuntimeDetails = { ...nodeOk, version: "21.9.0" }; const unknown: RuntimeDetails = { kind: "unknown", @@ -53,22 +53,22 @@ describe("runtime-guard", () => { expect(runtimeSatisfies(nodeOld)).toBe(false); expect(runtimeSatisfies(nodeTooOld)).toBe(false); expect(runtimeSatisfies(unknown)).toBe(false); - expect(isSupportedNodeVersion("22.14.0")).toBe(true); - expect(isSupportedNodeVersion("22.13.9")).toBe(false); + expect(isSupportedNodeVersion("22.16.0")).toBe(true); + expect(isSupportedNodeVersion("22.15.9")).toBe(false); expect(isSupportedNodeVersion(null)).toBe(false); }); it("parses simple minimum node engine ranges", () => { - expect(parseMinimumNodeEngine(">=22.14.0")).toEqual({ major: 22, minor: 14, patch: 0 }); + expect(parseMinimumNodeEngine(">=22.16.0")).toEqual({ major: 22, minor: 16, patch: 0 }); expect(parseMinimumNodeEngine(" >=v24.0.0 ")).toEqual({ major: 24, minor: 0, patch: 0 }); - expect(parseMinimumNodeEngine("^22.14.0")).toBeNull(); + expect(parseMinimumNodeEngine("^22.16.0")).toBeNull(); }); it("checks node versions against simple engine ranges", () => { - expect(nodeVersionSatisfiesEngine("22.14.0", ">=22.14.0")).toBe(true); - expect(nodeVersionSatisfiesEngine("22.13.9", ">=22.14.0")).toBe(false); - expect(nodeVersionSatisfiesEngine("24.0.0", ">=22.14.0")).toBe(true); - expect(nodeVersionSatisfiesEngine("22.14.0", "^22.14.0")).toBeNull(); + expect(nodeVersionSatisfiesEngine("22.16.0", ">=22.16.0")).toBe(true); + expect(nodeVersionSatisfiesEngine("22.15.9", ">=22.16.0")).toBe(false); + expect(nodeVersionSatisfiesEngine("24.0.0", ">=22.16.0")).toBe(true); + expect(nodeVersionSatisfiesEngine("22.16.0", "^22.16.0")).toBeNull(); }); it("throws via exit when runtime is too old", () => { @@ -99,7 +99,7 @@ describe("runtime-guard", () => { const details: RuntimeDetails = { ...detectRuntime(), kind: "node", - version: "22.14.0", + version: "22.16.0", execPath: "/usr/bin/node", }; expect(() => assertSupportedRuntime(runtime, details)).not.toThrow(); diff --git a/src/infra/runtime-guard.ts b/src/infra/runtime-guard.ts index 5e9d6b43329f..8d312c2dd8f2 100644 --- a/src/infra/runtime-guard.ts +++ b/src/infra/runtime-guard.ts @@ -9,7 +9,7 @@ type Semver = { patch: number; }; -const MIN_NODE: Semver = { major: 22, minor: 14, patch: 0 }; +const MIN_NODE: Semver = { major: 22, minor: 16, patch: 0 }; const MINIMUM_ENGINE_RE = /^\s*>=\s*v?(\d+\.\d+\.\d+)\s*$/i; export type RuntimeDetails = { @@ -111,7 +111,7 @@ export function assertSupportedRuntime( runtime.error( [ - "openclaw requires Node >=22.14.0.", + "openclaw requires Node >=22.16.0.", `Detected: ${runtimeLabel} (exec: ${execLabel}).`, `PATH searched: ${details.pathEnv}`, "Install Node: https://nodejs.org/en/download", diff --git a/src/infra/update-check.test.ts b/src/infra/update-check.test.ts index b85c620ae0f0..f6ef446857ef 100644 --- a/src/infra/update-check.test.ts +++ b/src/infra/update-check.test.ts @@ -58,7 +58,7 @@ describe("resolveNpmChannelTag", () => { status: version != null ? 200 : 404, json: async () => ({ version, - engines: version != null ? { node: ">=22.14.0" } : undefined, + engines: version != null ? { node: ">=22.16.0" } : undefined, }), } as Response; }), @@ -113,7 +113,7 @@ describe("resolveNpmChannelTag", () => { ).resolves.toEqual({ target: "latest", version: "1.0.4", - nodeEngine: ">=22.14.0", + nodeEngine: ">=22.16.0", }); await expect(fetchNpmTagVersion({ tag: "latest", timeoutMs: 1000 })).resolves.toEqual({ tag: "latest",