fix(cli): rebuild better-sqlite3 on install + quieter ABI-mismatch warning (closes #1822) (#1824)

* fix(cli): rebuild better-sqlite3 when binding is missing

* fix(cli): support Windows postinstall rebuild shims

---------

Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>
This commit is contained in:
i-trytoohard 2026-05-18 04:02:26 +05:30 committed by GitHub
parent 87c6a3d9f4
commit d5d0f077ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 352 additions and 41 deletions

View File

@ -0,0 +1,7 @@
---
"@aoagents/ao": patch
"@aoagents/ao-core": patch
"@aoagents/ao-cli": patch
---
Rebuild missing better-sqlite3 native bindings during ao postinstall and replace noisy activity-events native-binding failures with a one-line diagnostic.

View File

@ -11,19 +11,28 @@
* If not (common with nvm/fnm/volta), rebuilds from source via npx node-gyp.
* See: https://github.com/ComposioHQ/agent-orchestrator/issues/987
*
* 3. Clears stale Next.js runtime cache (.next/cache) from @composio/ao-web
* 3. Verifies better-sqlite3 has a native binding for this Node ABI.
* Node majors can ship new NODE_MODULE_VERSION values before better-sqlite3
* publishes matching prebuilds; global installs must rebuild from source.
* See: https://github.com/ComposioHQ/agent-orchestrator/issues/1822
*
* 4. Clears stale Next.js runtime cache (.next/cache) from @composio/ao-web
* after a version upgrade, so `ao start` serves fresh dashboard assets.
* Writes a version stamp (.next/AO_VERSION) to skip cleanup on subsequent runs.
*/
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";
import { execFileSync, execSync } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
function findPackageUp(startDir, ...segments) {
function isWindows() {
return process.platform === "win32";
}
export function findPackageUp(startDir, ...segments) {
let dir = resolve(startDir);
while (true) {
const candidate = resolve(dir, "node_modules", ...segments);
@ -35,12 +44,12 @@ function findPackageUp(startDir, ...segments) {
return null;
}
function resolveNodeModulesPackage(fromDir, ...segments) {
export function resolveNodeModulesPackage(fromDir, ...segments) {
const packageDir = resolve(fromDir, "node_modules", ...segments);
return existsSync(resolve(packageDir, "package.json")) ? packageDir : null;
}
function findWebDir() {
export function findWebDir() {
const directWebDir = findPackageUp(__dirname, "@aoagents", "ao-web");
if (directWebDir) return directWebDir;
@ -50,8 +59,112 @@ function findWebDir() {
return resolveNodeModulesPackage(cliDir, "@aoagents", "ao-web");
}
// --- 1 & 2. Fix node-pty spawn-helper permissions and verify ABI (non-Windows only) ---
if (process.platform !== "win32") {
export function findBetterSqlite3Dir() {
const directBetterSqlite3Dir = findPackageUp(__dirname, "better-sqlite3");
if (directBetterSqlite3Dir) return directBetterSqlite3Dir;
const cliDir = findPackageUp(__dirname, "@aoagents", "ao-cli");
if (!cliDir) return null;
const coreDir = resolveNodeModulesPackage(cliDir, "@aoagents", "ao-core");
if (!coreDir) return null;
return (
resolveNodeModulesPackage(coreDir, "better-sqlite3") ?? findPackageUp(coreDir, "better-sqlite3")
);
}
export function betterSqlite3BindingCandidates(
packageDir,
{
platform = process.platform,
arch = process.arch,
modules = process.versions.modules,
nodeVersion = process.versions.node,
} = {},
) {
return [
resolve(packageDir, "build", "better_sqlite3.node"),
resolve(packageDir, "build", "Debug", "better_sqlite3.node"),
resolve(packageDir, "build", "Release", "better_sqlite3.node"),
resolve(packageDir, "out", "Debug", "better_sqlite3.node"),
resolve(packageDir, "Debug", "better_sqlite3.node"),
resolve(packageDir, "out", "Release", "better_sqlite3.node"),
resolve(packageDir, "Release", "better_sqlite3.node"),
resolve(packageDir, "build", "default", "better_sqlite3.node"),
resolve(packageDir, "compiled", nodeVersion, platform, arch, "better_sqlite3.node"),
resolve(packageDir, "addon-build", "release", "install-root", "better_sqlite3.node"),
resolve(packageDir, "addon-build", "debug", "install-root", "better_sqlite3.node"),
resolve(packageDir, "addon-build", "default", "install-root", "better_sqlite3.node"),
resolve(
packageDir,
"lib",
"binding",
`node-v${modules}-${platform}-${arch}`,
"better_sqlite3.node",
),
];
}
export function hasBetterSqlite3Binding(packageDir, options = {}) {
const fileExists = options.existsSync ?? existsSync;
return betterSqlite3BindingCandidates(packageDir, options).some((candidate) =>
fileExists(candidate),
);
}
export function betterSqlite3RebuildCommand(packageDir, env = process.env) {
const packageManager =
`${env.npm_config_user_agent ?? ""} ${env.npm_execpath ?? ""}`.toLowerCase();
if (packageManager.includes("npm") && !packageManager.includes("pnpm")) {
return { command: "npm", args: ["rebuild"], display: `cd ${packageDir} && npm rebuild` };
}
return {
command: "pnpm",
args: ["--dir", packageDir, "rebuild"],
display: `pnpm --dir ${packageDir} rebuild`,
};
}
function checkBetterSqlite3Binding() {
const betterSqlite3Dir = findBetterSqlite3Dir();
if (!betterSqlite3Dir) {
console.warn(
"⚠️ better-sqlite3 package not found; skipping activity-events native binding check",
);
return;
}
const abi = process.versions.modules;
if (hasBetterSqlite3Binding(betterSqlite3Dir)) {
console.log(
`✓ better-sqlite3 native binding present for Node ${process.version} (ABI v${abi})`,
);
return;
}
const { command, args, display } = betterSqlite3RebuildCommand(betterSqlite3Dir);
try {
execFileSync(command, args, {
cwd: betterSqlite3Dir,
stdio: "ignore",
timeout: 120000,
shell: isWindows(),
windowsHide: true,
});
console.log(
`✓ better-sqlite3 native binding rebuilt for Node ${process.version} (ABI v${abi})`,
);
} catch {
console.warn(
`⚠️ better-sqlite3 rebuild failed for Node ${process.version} (ABI v${abi}) — activity events may be unavailable. Manual fix: ${display}`,
);
}
}
function fixNodePty() {
if (isWindows()) return;
const nodePtyDir = findPackageUp(__dirname, "node-pty");
if (nodePtyDir) {
const spawnHelper = resolve(
@ -84,7 +197,11 @@ if (process.platform !== "win32") {
},
);
} catch {
console.log("⚠️ node-pty prebuilt binary incompatible with Node.js " + process.version + ", rebuilding...");
console.log(
"⚠️ node-pty prebuilt binary incompatible with Node.js " +
process.version +
", rebuilding...",
);
try {
execSync("npx --yes node-gyp rebuild", {
cwd: nodePtyDir,
@ -100,27 +217,43 @@ if (process.platform !== "win32") {
}
}
// --- 3. Clear stale Next.js runtime cache after version upgrade ---
try {
const webDir = findWebDir();
if (webDir) {
const pkgPath = resolve(webDir, "package.json");
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const version = pkg.version;
const cacheDir = resolve(webDir, ".next", "cache");
const stampPath = resolve(webDir, ".next", "AO_VERSION");
function clearDashboardCache() {
try {
const webDir = findWebDir();
if (webDir) {
const pkgPath = resolve(webDir, "package.json");
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const version = pkg.version;
const cacheDir = resolve(webDir, ".next", "cache");
const stampPath = resolve(webDir, ".next", "AO_VERSION");
if (existsSync(cacheDir)) {
rmSync(cacheDir, { recursive: true, force: true });
console.log("✓ Cleared stale .next/cache");
}
if (existsSync(resolve(webDir, ".next"))) {
writeFileSync(stampPath, version, "utf8");
console.log(`✓ Dashboard version stamp set to ${version}`);
if (existsSync(cacheDir)) {
rmSync(cacheDir, { recursive: true, force: true });
console.log("✓ Cleared stale .next/cache");
}
if (existsSync(resolve(webDir, ".next"))) {
writeFileSync(stampPath, version, "utf8");
console.log(`✓ Dashboard version stamp set to ${version}`);
}
}
}
} catch (err) {
console.warn(`⚠️ Could not clear dashboard cache (non-critical): ${err.message}`);
}
} catch (err) {
console.warn(`⚠️ Could not clear dashboard cache (non-critical): ${err.message}`);
}
export function runPostinstall() {
// --- 1 & 2. Fix node-pty spawn-helper permissions and verify ABI (non-Windows only) ---
fixNodePty();
// --- 3. Ensure better-sqlite3 has a native binding for this Node ABI ---
checkBetterSqlite3Binding();
// --- 4. Clear stale Next.js runtime cache after version upgrade ---
clearDashboardCache();
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
runPostinstall();
}

View File

@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
betterSqlite3BindingCandidates,
betterSqlite3RebuildCommand,
hasBetterSqlite3Binding,
} from "../../../ao/bin/postinstall.js";
describe("ao postinstall better-sqlite3 native binding detection", () => {
const env = {
platform: "darwin",
arch: "arm64",
modules: "141",
nodeVersion: "25.9.0",
};
it("checks the current Node ABI binding path", () => {
const packageDir = "virtual-better-sqlite3";
const candidates = betterSqlite3BindingCandidates(packageDir, env);
expect(candidates.some((candidate) => candidate.includes("node-v141-darwin-arm64"))).toBe(true);
});
it("reports the binding present when a mocked candidate exists", () => {
const packageDir = "virtual-better-sqlite3";
const candidates = betterSqlite3BindingCandidates(packageDir, env);
const currentAbiBinding = candidates.find((candidate) =>
candidate.includes("node-v141-darwin-arm64"),
);
if (!currentAbiBinding) {
throw new Error("expected current ABI binding candidate");
}
const existingFiles = new Set([currentAbiBinding]);
expect(
hasBetterSqlite3Binding(packageDir, {
...env,
existsSync: (candidate: string) => existingFiles.has(candidate),
}),
).toBe(true);
});
it("reports the binding missing when no mocked candidate exists", () => {
expect(
hasBetterSqlite3Binding("virtual-better-sqlite3", {
...env,
existsSync: () => false,
}),
).toBe(false);
});
it("uses pnpm to rebuild inside the resolved better-sqlite3 package", () => {
expect(
betterSqlite3RebuildCommand("virtual-better-sqlite3", {
npm_config_user_agent: "pnpm/9.15.4 npm/? node/v25.9.0 darwin arm64",
}),
).toEqual({
command: "pnpm",
args: ["--dir", "virtual-better-sqlite3", "rebuild"],
display: "pnpm --dir virtual-better-sqlite3 rebuild",
});
});
});

View File

@ -91,7 +91,7 @@
"zod": "^3.24.0"
},
"optionalDependencies": {
"better-sqlite3": "^11.0.0"
"better-sqlite3": "^12.10.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^12.3.0",
@ -99,8 +99,8 @@
"@types/node": "^25.2.3",
"@vitest/coverage-v8": "^4.0.18",
"rollup": "^4.60.1",
"tsx": "^4.21.0",
"tslib": "^2.8.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"vitest": "^4.0.18"
},

View File

@ -0,0 +1,72 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
__resetActivityEventsDbWarningForTests,
emitActivityEventsDbUnavailableWarning,
formatActivityEventsDbUnavailableWarning,
} from "../events-db.js";
describe("activity-events DB unavailable warning", () => {
const originalArgv = process.argv;
const originalDebug = process.env["AO_DEBUG"];
beforeEach(() => {
__resetActivityEventsDbWarningForTests();
vi.spyOn(console, "warn").mockImplementation(() => {});
delete process.env["AO_DEBUG"];
process.argv = ["node", "ao"];
});
afterEach(() => {
process.argv = originalArgv;
if (originalDebug === undefined) {
delete process.env["AO_DEBUG"];
} else {
process.env["AO_DEBUG"] = originalDebug;
}
vi.restoreAllMocks();
});
it("formats missing native binding errors without the bindings search path", () => {
const message = formatActivityEventsDbUnavailableWarning(
new Error(
"Could not locate the bindings file. Tried:\n → /tmp/build/Release/better_sqlite3.node",
),
);
expect(message).toBe(
`[ao] activity-events disabled: better-sqlite3 not compiled for Node ${process.version} (ABI v${process.versions.modules}). Run \`pnpm rebuild better-sqlite3\` or use a supported Node version.`,
);
expect(message).not.toContain("Tried:");
expect(message).not.toContain("/tmp/build/Release");
});
it("prints the runtime warning once per process", () => {
process.env["AO_DEBUG"] = "1";
const err = new Error(
"Could not locate the bindings file. Tried:\n → /tmp/better_sqlite3.node",
);
emitActivityEventsDbUnavailableWarning(err);
emitActivityEventsDbUnavailableWarning(err);
expect(console.warn).toHaveBeenCalledTimes(1);
expect(vi.mocked(console.warn).mock.calls[0]?.[0]).toContain(
"activity-events disabled: better-sqlite3 not compiled",
);
});
it("suppresses non-events invocations unless AO_DEBUG=1", () => {
const err = new Error(
"Could not locate the bindings file. Tried:\n → /tmp/better_sqlite3.node",
);
process.argv = ["node", "ao", "spawn", "demo"];
emitActivityEventsDbUnavailableWarning(err);
expect(console.warn).not.toHaveBeenCalled();
process.argv = ["node", "ao", "events", "stats"];
emitActivityEventsDbUnavailableWarning(err);
expect(console.warn).toHaveBeenCalledTimes(1);
});
});

View File

@ -24,6 +24,7 @@ type BetterSqlite3Database = {
let _db: BetterSqlite3Database | null = null;
let _dbFailed = false;
let _ftsEnabled = false;
let _dbUnavailableWarningEmitted = false;
const PRUNE_BATCH_SIZE = 1000;
function getEventsDbPath(): string {
@ -90,14 +91,12 @@ function initFts(db: BetterSqlite3Database): void {
}
function pruneOldEvents(db: BetterSqlite3Database, cutoff: number): void {
db
.prepare(
`DELETE FROM activity_events
db.prepare(
`DELETE FROM activity_events
WHERE rowid IN (
SELECT rowid FROM activity_events WHERE ts_epoch < ? LIMIT ?
)`,
)
.run(cutoff, PRUNE_BATCH_SIZE);
).run(cutoff, PRUNE_BATCH_SIZE);
}
function openDb(): BetterSqlite3Database {
@ -161,6 +160,42 @@ export function closeDb(): void {
_ftsEnabled = false;
}
function isAoEventsInvocation(argv = process.argv): boolean {
return argv.slice(2).includes("events");
}
function isMissingBetterSqlite3Binding(err: unknown): boolean {
const message = err instanceof Error ? err.message : String(err);
return (
message.includes("Could not locate the bindings file") ||
message.includes("better_sqlite3.node") ||
message.includes("Cannot find module 'better-sqlite3'")
);
}
function firstErrorLine(err: unknown): string {
return (err instanceof Error ? err.message : String(err)).split(/\r?\n/, 1)[0] ?? "unknown error";
}
export function formatActivityEventsDbUnavailableWarning(err: unknown): string {
if (isMissingBetterSqlite3Binding(err)) {
return `[ao] activity-events disabled: better-sqlite3 not compiled for Node ${process.version} (ABI v${process.versions.modules}). Run \`pnpm rebuild better-sqlite3\` or use a supported Node version.`;
}
return `[ao] activity-events disabled: better-sqlite3 failed to load: ${firstErrorLine(err)}`;
}
export function emitActivityEventsDbUnavailableWarning(err: unknown): void {
if (_dbUnavailableWarningEmitted) return;
if (process.env["AO_DEBUG"] !== "1" && !isAoEventsInvocation()) return;
_dbUnavailableWarningEmitted = true;
// eslint-disable-next-line no-console
console.warn(formatActivityEventsDbUnavailableWarning(err));
}
export function __resetActivityEventsDbWarningForTests(): void {
_dbUnavailableWarningEmitted = false;
}
/**
* Get the lazily-initialized DB connection.
* Returns null if better-sqlite3 failed to load or init callers should treat null as no-op.
@ -173,8 +208,7 @@ export function getDb(): BetterSqlite3Database | null {
return _db;
} catch (err) {
_dbFailed = true;
// Log once so operators know events are being dropped; subsequent calls return null silently.
console.warn("[ao] activity-events DB unavailable — events will be dropped:", err instanceof Error ? err.message : String(err));
emitActivityEventsDbUnavailableWarning(err);
return null;
}
}

View File

@ -163,8 +163,8 @@ importers:
version: 3.25.76
optionalDependencies:
better-sqlite3:
specifier: ^11.0.0
version: 11.10.0
specifier: ^12.10.0
version: 12.10.0
devDependencies:
'@rollup/plugin-typescript':
specifier: ^12.3.0
@ -2352,8 +2352,9 @@ packages:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
better-sqlite3@11.10.0:
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
better-sqlite3@12.10.0:
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@ -5917,7 +5918,7 @@ snapshots:
dependencies:
is-windows: 1.0.2
better-sqlite3@11.10.0:
better-sqlite3@12.10.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3