diff --git a/packages/storage/src/__tests__/package-import.test.ts b/packages/storage/src/__tests__/package-import.test.ts new file mode 100644 index 0000000000..f4f6c67fa6 --- /dev/null +++ b/packages/storage/src/__tests__/package-import.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { test } from 'node:test'; + +/** + * Regression guard for #1257. Restored after #2710 removed it; the guard's + * absence let three static `node:sqlite` value imports back into the + * `@maka/storage` barrel, so `maka --help` printed Node's SQLite + * ExperimentalWarning again. + */ +function runModule(source: string): { status: number | null; stderr: string } { + const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], { + encoding: 'utf8', + }); + return { status: result.status, stderr: result.stderr }; +} + +test('top-level package import does not initialize node:sqlite', () => { + const packageEntry = new URL('../index.js', import.meta.url).href; + const result = runModule(`await import(${JSON.stringify(packageEntry)})`); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, ''); +}); + +test('constructing a SQLite store stays silent on stderr', () => { + const loader = new URL('../sqlite-module.js', import.meta.url).href; + const result = runModule( + `const { loadDatabaseSync } = await import(${JSON.stringify(loader)});` + + `new (loadDatabaseSync())(':memory:').close();`, + ); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, ''); +}); + +test('no compiled module statically imports node:sqlite', async () => { + const { readdir, readFile } = await import('node:fs/promises'); + // Tests run against dist/, where `import type` has already been erased — so + // any surviving `node:sqlite` specifier is a real value load. + const distRoot = new URL('../', import.meta.url); + const offenders: string[] = []; + for (const entry of await readdir(distRoot)) { + if (!entry.endsWith('.js') || entry === 'sqlite-module.js') continue; + const text = await readFile(new URL(entry, distRoot), 'utf8'); + if (text.includes("'node:sqlite'") || text.includes('"node:sqlite"')) { + offenders.push(entry); + } + } + assert.deepEqual(offenders, []); +}); diff --git a/packages/storage/src/codex-session-adapter.ts b/packages/storage/src/codex-session-adapter.ts index 74ec33a0bb..21e43c1613 100644 --- a/packages/storage/src/codex-session-adapter.ts +++ b/packages/storage/src/codex-session-adapter.ts @@ -10,6 +10,7 @@ import type { ExternalSessionQuery, ExternalSessionSummary, } from '@maka/core/external-session'; +import { loadSqliteModule } from './sqlite-module.js'; export const CODEX_SESSION_ADAPTER_ID = 'codex'; export const CODEX_ROLLOUT_MAX_BYTES = 64 * 1024 * 1024; @@ -521,7 +522,7 @@ async function readCodexThreadRows( exactId?: string, ): Promise { try { - const sqlite = await import('node:sqlite'); + const sqlite = loadSqliteModule(); const db = new sqlite.DatabaseSync(dbPath, { readOnly: true }); try { const columns = new Set( diff --git a/packages/storage/src/foreign-session-store.ts b/packages/storage/src/foreign-session-store.ts index ccebb769e8..bf1e1d81b7 100644 --- a/packages/storage/src/foreign-session-store.ts +++ b/packages/storage/src/foreign-session-store.ts @@ -57,6 +57,7 @@ import { type ForeignSessionSource, type ForeignSessionSummary, } from '@maka/core/foreign-session'; +import { loadSqliteModule } from './sqlite-module.js'; export interface ForeignSessionScanOptions { /** Only sessions whose recorded cwd equals this path (after realpath-free @@ -536,7 +537,7 @@ async function readCodexThreadRows( cwdFilter?: string, ): Promise { try { - const sqlite = await import('node:sqlite'); + const sqlite = loadSqliteModule(); const db = new sqlite.DatabaseSync(dbPath, { readOnly: true }); try { const columns = new Set( diff --git a/packages/storage/src/managed-dependency-environment.ts b/packages/storage/src/managed-dependency-environment.ts index fa548f18b6..be3e345ce9 100644 --- a/packages/storage/src/managed-dependency-environment.ts +++ b/packages/storage/src/managed-dependency-environment.ts @@ -1,7 +1,6 @@ import { execFile } from 'node:child_process'; import { createHash, randomUUID } from 'node:crypto'; import { createReadStream } from 'node:fs'; -import { createRequire } from 'node:module'; import { chmod, cp, @@ -19,6 +18,7 @@ import { } from 'node:fs/promises'; import { dirname, isAbsolute, join, normalize, posix, relative, resolve } from 'node:path'; import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import { tryLock, unlock } from 'fs-native-extensions'; const MANAGED_DEPENDENCY_IDENTITY_DOMAIN = 'maka.managed_dependency_environment.v1\0'; @@ -59,7 +59,6 @@ const RECEIPT_KEYS = [ 'contentBytes', 'contentEntries', ] as const; -const require = createRequire(import.meta.url); export type ManagedDependencyPackageManager = 'npm' | 'pnpm' | 'yarn'; @@ -202,7 +201,7 @@ interface DependencyAuthorityOwnerLock { } function openDependencyReceiptAuthority(path: string): DependencyReceiptAuthority { - const Database = (require('node:sqlite') as typeof import('node:sqlite')).DatabaseSync; + const Database = loadDatabaseSync(); const database: DatabaseSync = new Database(path); database.exec('PRAGMA synchronous = FULL'); const version = Number( diff --git a/packages/storage/src/operational-state-backup.ts b/packages/storage/src/operational-state-backup.ts index ab69f810bb..62fdf0a274 100644 --- a/packages/storage/src/operational-state-backup.ts +++ b/packages/storage/src/operational-state-backup.ts @@ -13,7 +13,8 @@ import { writeFile, } from 'node:fs/promises'; import { dirname, relative, resolve } from 'node:path'; -import { DatabaseSync } from 'node:sqlite'; +import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import { decodeArtifactRecordJsons } from './artifact-metadata-codec.js'; import { withArtifactWriterLock } from './artifact-writer-lock.js'; import { decodeStoredMessage } from './execution-record-codec.js'; @@ -326,7 +327,7 @@ function validateSqlite(path: string, files: readonly OperationalBackupFile[]): if (!metadata.isFile() || metadata.isSymbolicLink()) { throw new Error('runtime.sqlite is not a regular file'); } - const database = new DatabaseSync(path, { readOnly: true }); + const database = new (loadDatabaseSync())(path, { readOnly: true }); try { database.exec('PRAGMA query_only = ON; PRAGMA foreign_keys = ON; BEGIN'); const integrity = database.prepare('PRAGMA integrity_check').all() as Array<{ @@ -491,7 +492,7 @@ function validateSqlite(path: string, files: readonly OperationalBackupFile[]): } function normalizeStandaloneSqliteSnapshot(path: string): void { - const database = new DatabaseSync(path); + const database = new (loadDatabaseSync())(path); try { const row = database.prepare('PRAGMA journal_mode = DELETE').get() as | { journal_mode?: unknown } diff --git a/packages/storage/src/operational-state-store.ts b/packages/storage/src/operational-state-store.ts index 4fed1c2772..bd598a9e06 100644 --- a/packages/storage/src/operational-state-store.ts +++ b/packages/storage/src/operational-state-store.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; -import { createRequire } from 'node:module'; import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync, loadSqliteModule } from './sqlite-module.js'; import { configureSqliteRuntimeDatabase, configureSqliteRuntimeLockWait, @@ -118,7 +118,6 @@ const RELEASED_CUTOVER_STORE_VALIDATION_KEYS: ReadonlyMap(); export interface OperationalStateDatabaseOptions { @@ -582,29 +581,6 @@ function registerSchema(db: DatabaseSync, scope: string, version: number, applie `).run(scope, version, appliedAt); } -function loadDatabaseSync(): typeof import('node:sqlite').DatabaseSync { - const emitWarning = process.emitWarning; - process.emitWarning = ((warning: string | Error, ...args: unknown[]) => { - const warningType = typeof args[0] === 'string' ? args[0] : undefined; - if ( - warningType === 'ExperimentalWarning' && - String(warning).startsWith('SQLite is an experimental feature') - ) { - return; - } - Reflect.apply(emitWarning, process, [warning, ...args]); - }) as typeof process.emitWarning; - try { - return (require('node:sqlite') as typeof import('node:sqlite')).DatabaseSync; - } finally { - process.emitWarning = emitWarning; - } -} - -function loadSqliteModule(): typeof import('node:sqlite') { - return require('node:sqlite') as typeof import('node:sqlite'); -} - function rollback(db: DatabaseSync): void { try { db.exec('ROLLBACK'); diff --git a/packages/storage/src/operational-target-schema.ts b/packages/storage/src/operational-target-schema.ts index 18bfd85718..4e686b15fb 100644 --- a/packages/storage/src/operational-target-schema.ts +++ b/packages/storage/src/operational-target-schema.ts @@ -1,4 +1,5 @@ -import { DatabaseSync } from 'node:sqlite'; +import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import { migrateSqliteArtifactDatabase } from './sqlite-artifact-schema.js'; import { migrateSqliteCoreExecutionDatabase } from './sqlite-core-execution-schema.js'; import { migrateSqliteRuntimeDatabase } from './sqlite-runtime-schema.js'; @@ -45,7 +46,7 @@ export function assertCurrentOperationalTargetSchema(database: DatabaseSync): vo } function buildOperationalTargetSchema(): ReadonlyMap { - const database = new DatabaseSync(':memory:'); + const database = new (loadDatabaseSync())(':memory:'); try { migrateSqliteRuntimeDatabase(database); migrateSqliteSessionMetadataDatabase(database); @@ -126,7 +127,7 @@ const RELEASED_LEGACY_RETIREMENT_DDL: ReadonlyMap = new Map([ let cachedLegacyRetirementSchema: ReadonlyMap> | undefined; function buildLegacyRetirementSchema(): ReadonlyMap> { - const database = new DatabaseSync(':memory:'); + const database = new (loadDatabaseSync())(':memory:'); try { database.exec('PRAGMA foreign_keys = OFF'); for (const ddl of RELEASED_LEGACY_RETIREMENT_DDL.values()) database.exec(ddl); diff --git a/packages/storage/src/session-bundle-policy.ts b/packages/storage/src/session-bundle-policy.ts index d21b56f731..180d6f7808 100644 --- a/packages/storage/src/session-bundle-policy.ts +++ b/packages/storage/src/session-bundle-policy.ts @@ -1,7 +1,8 @@ import { copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm } from 'node:fs/promises'; import { randomUUID } from 'node:crypto'; import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; -import { DatabaseSync } from 'node:sqlite'; +import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import type { ArtifactRecord } from '@maka/core/artifacts'; import { decodeArtifactRecordJsons } from './artifact-metadata-codec.js'; import { withArtifactWriterLock } from './artifact-writer-lock.js'; @@ -81,7 +82,7 @@ export async function planSessionBundleExport( const databasePath = resolve(stateRoot, OPERATIONAL_STATE_DATABASE_NAME); await assertRegularFile(databasePath, OPERATIONAL_STATE_DATABASE_NAME); - const database = new DatabaseSync(databasePath, { readOnly: true }); + const database = new (loadDatabaseSync())(databasePath, { readOnly: true }); let artifacts: ArtifactRecord[]; try { const session = database @@ -182,7 +183,7 @@ async function exportFilteredDatabase( } finally { lease.close(); } - const database = new DatabaseSync(destinationPath); + const database = new (loadDatabaseSync())(destinationPath); try { database.exec('PRAGMA foreign_keys = OFF; BEGIN IMMEDIATE'); const tables = database diff --git a/packages/storage/src/sqlite-long-term-memory-store.ts b/packages/storage/src/sqlite-long-term-memory-store.ts index a0d0aeb1f2..7f5647c567 100644 --- a/packages/storage/src/sqlite-long-term-memory-store.ts +++ b/packages/storage/src/sqlite-long-term-memory-store.ts @@ -7,8 +7,8 @@ import { lstatSync, openSync, } from 'node:fs'; -import { createRequire } from 'node:module'; import type { DatabaseSync } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import { MemoryItemStoreConflictError, isMemoryItemKind, @@ -60,7 +60,6 @@ const MAX_IDENTIFIER_CODE_POINTS = 512; const MAX_KEY_CODE_POINTS = 256; const MAX_OPERATION_RESULT_JSON_CODE_UNITS = 128 * 1_024; -const require = createRequire(import.meta.url); export type SqliteMemoryItemStoreFailpoint = | 'after_item_write' @@ -1153,10 +1152,6 @@ export class SqliteMemoryItemStore implements MemoryItemStore { } } -function loadDatabaseSync(): typeof import('node:sqlite').DatabaseSync { - return (require('node:sqlite') as typeof import('node:sqlite')).DatabaseSync; -} - export function buildSqliteMemoryKeySearchQuery(input: { readonly terms: readonly string[]; readonly match: 'exact' | 'prefix'; diff --git a/packages/storage/src/sqlite-module.ts b/packages/storage/src/sqlite-module.ts new file mode 100644 index 0000000000..25168e14e1 --- /dev/null +++ b/packages/storage/src/sqlite-module.ts @@ -0,0 +1,48 @@ +/** + * The single seam through which this package loads `node:sqlite`. + * + * Two rules, both load-bearing: + * + * 1. LAZY. `node:sqlite` is never a static value import. Node evaluates the + * builtin the moment it enters a module graph, so a static import makes + * every consumer of the `@maka/storage` barrel pay for SQLite — including + * `maka --help`, which never opens a database. That is the regression + * issue #1257 / PR #1258 fixed once already. + * 2. QUIET. Evaluating the builtin emits Node's + * `ExperimentalWarning: SQLite is an experimental feature` on stderr. + * Maka's storage layer is SQLite by design, so the warning carries no + * decision for the user; it only corrupts the stderr of every CLI + * invocation and of subprocess tests that assert clean output. Suppress + * exactly that one warning, for exactly the duration of the load, and + * leave every other warning — deprecations included — untouched. + */ +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +let cached: typeof import('node:sqlite') | undefined; + +export function loadSqliteModule(): typeof import('node:sqlite') { + if (cached) return cached; + const emitWarning = process.emitWarning; + process.emitWarning = ((warning: string | Error, ...args: unknown[]) => { + const warningType = typeof args[0] === 'string' ? args[0] : undefined; + if ( + warningType === 'ExperimentalWarning' && + String(warning).startsWith('SQLite is an experimental feature') + ) { + return; + } + Reflect.apply(emitWarning, process, [warning, ...args]); + }) as typeof process.emitWarning; + try { + cached = require('node:sqlite') as typeof import('node:sqlite'); + return cached; + } finally { + process.emitWarning = emitWarning; + } +} + +export function loadDatabaseSync(): typeof import('node:sqlite').DatabaseSync { + return loadSqliteModule().DatabaseSync; +} diff --git a/packages/storage/src/sqlite-runtime-store.ts b/packages/storage/src/sqlite-runtime-store.ts index b94421ff1c..82307c9451 100644 --- a/packages/storage/src/sqlite-runtime-store.ts +++ b/packages/storage/src/sqlite-runtime-store.ts @@ -1,8 +1,8 @@ import { createHash } from 'node:crypto'; import { mkdirSync } from 'node:fs'; -import { createRequire } from 'node:module'; import { dirname } from 'node:path'; import type { DatabaseSync, SQLInputValue } from 'node:sqlite'; +import { loadDatabaseSync } from './sqlite-module.js'; import { isDeepStrictEqual } from 'node:util'; import { buildWorkspaceBaselineAuthorityEvents, @@ -110,11 +110,6 @@ function requireRuntimeEventScanCount(value: unknown): number { return value as number; } -const require = createRequire(import.meta.url); - -function loadDatabaseSync(): typeof import('node:sqlite').DatabaseSync { - return (require('node:sqlite') as typeof import('node:sqlite')).DatabaseSync; -} function configureSqliteRuntimeReadOnlyDatabase(db: DatabaseSync): void { db.exec('PRAGMA busy_timeout = 5000'); diff --git a/packages/storage/src/sqlite-session-metadata-store.ts b/packages/storage/src/sqlite-session-metadata-store.ts index c8fdbc341e..456fd8a514 100644 --- a/packages/storage/src/sqlite-session-metadata-store.ts +++ b/packages/storage/src/sqlite-session-metadata-store.ts @@ -1,10 +1,10 @@ -import { createRequire } from 'node:module'; import { createHash } from 'node:crypto'; import { dirname, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { existsSync, mkdirSync } from 'node:fs'; import { isDeepStrictEqual } from 'node:util'; import type { DatabaseSync } from 'node:sqlite'; +import { loadSqliteModule } from './sqlite-module.js'; import { AGENT_GRAPH_CLIENT_PROJECTION_SCHEMA_VERSION, AgentGraphClientProjectionConflictError, @@ -126,30 +126,10 @@ const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES = 1_024; const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES = 4 * 1024 * 1024; const SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES = 32; -const require = createRequire(import.meta.url); const AGENT_GRAPH_CONTROL_DELETE_TABLES = SQLITE_AGENT_GRAPH_CONTROL_TABLES.filter( (table) => table !== 'agent_graph_epochs', ).reverse(); -function loadSqliteModule(): typeof import('node:sqlite') { - const emitWarning = process.emitWarning; - process.emitWarning = ((warning: string | Error, ...args: unknown[]) => { - const warningType = typeof args[0] === 'string' ? args[0] : undefined; - if ( - warningType === 'ExperimentalWarning' && - String(warning).startsWith('SQLite is an experimental feature') - ) { - return; - } - Reflect.apply(emitWarning, process, [warning, ...args]); - }) as typeof process.emitWarning; - try { - return require('node:sqlite') as typeof import('node:sqlite'); - } finally { - process.emitWarning = emitWarning; - } -} - export type SqliteSessionMetadataStoreFailpoint = | 'after_session_row_write' | 'after_agent_graph_intent_claim_write'