pi-warden 0.74.0 → 0.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ How to keep this current: add the entry in the same pull request as the change,
8
8
 
9
9
  <!-- Empty. Next release starts here. -->
10
10
 
11
+ ## 0.74.1
12
+
13
+ ### Fixed
14
+
15
+ - Learning features work when Pi runs on Bun. Pi's release binaries (`pi-linux-x64.tar.gz` and the rest) are Bun `--compile` executables in which `import("node:sqlite")` fails with `No such built-in module: node:sqlite`, so `holds.db` never opened and learning was off for every release-binary user. `node:sqlite` is still tried first and is unchanged; only when that import fails and the process runs on Bun does `src/sqlite-adapter.ts` open the database through `bun:sqlite` behind the `DatabaseSync` subset learning uses. With neither module loading, learning stays off behind one warning that now names both modules.
16
+
11
17
  ## 0.74.0
12
18
 
13
19
  ### Added
@@ -1,6 +1,9 @@
1
+ import type { SqliteDriver } from "./sqlite-adapter.js";
1
2
  import type { HostDirs } from "./host-dirs.js";
2
3
  import type { CallScores } from "./holds.js";
3
4
  export declare const HOLDS_SCHEMA = "\n CREATE TABLE IF NOT EXISTS holds (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n project_root TEXT NOT NULL,\n tool TEXT NOT NULL,\n signature_hash TEXT NOT NULL,\n command_preview TEXT,\n task TEXT,\n plan TEXT,\n context_summary TEXT,\n preceding_actions TEXT,\n scores TEXT NOT NULL,\n level TEXT NOT NULL,\n held INTEGER NOT NULL,\n reasons TEXT,\n agent_reason TEXT,\n outcome TEXT,\n outcome_at INTEGER,\n confidence REAL\n );\n CREATE INDEX IF NOT EXISTS idx_holds_project_signature ON holds(project_root, signature_hash);\n CREATE INDEX IF NOT EXISTS idx_holds_outcome ON holds(outcome);\n CREATE INDEX IF NOT EXISTS idx_holds_timestamp ON holds(timestamp);\n";
5
+ /** Which module opened the learning database: "node:sqlite", "bun:sqlite", or undefined while learning is off. */
6
+ export declare function sqliteDriver(): SqliteDriver | undefined;
4
7
  export declare function initSchema(retentionDays?: number, dirs?: HostDirs): Promise<void>;
5
8
  export interface HoldScores {
6
9
  irreversible: number;
package/dist/learning.js CHANGED
@@ -3,11 +3,14 @@
3
3
  import { createHash } from "crypto";
4
4
  import { mkdirSync } from "fs";
5
5
  import { dirname, join } from "path";
6
+ import { isBunRuntime, openBunSqlite, SqliteUnavailableError } from "./sqlite-adapter.js";
6
7
  import { defaultHostDirs } from "./host-dirs.js";
7
8
  import { userConfigPath } from "./config.js";
8
9
  import { redact } from "./redact.js";
9
10
  const dbs = new Map();
10
11
  let sqliteAvailable;
12
+ /** Which module opened the learning database; undefined while learning is off. */
13
+ let driver;
11
14
  // --- Schema (shared constant) ---
12
15
  export const HOLDS_SCHEMA = `
13
16
  CREATE TABLE IF NOT EXISTS holds (
@@ -39,6 +42,33 @@ const NOOP_DB = {
39
42
  prepare() { return { run() { return { changes: 0, lastInsertRowid: 0 }; }, get() { return undefined; }, all() { return []; } }; },
40
43
  pragma() { },
41
44
  };
45
+ /** Learning stays off: one warning names both candidate modules so the report says what to look at. */
46
+ function disableLearning(detail) {
47
+ sqliteAvailable = false;
48
+ console.warn("pi-warden: node:sqlite unavailable, bun:sqlite unavailable, learning features disabled:", detail);
49
+ return NOOP_DB;
50
+ }
51
+ /** Pi's release binaries are Bun --compile executables, where node:sqlite may not be a built-in
52
+ * module while bun:sqlite always is. On Bun the database opens through the adapter; with neither
53
+ * module loading, learning stays off behind one warning. */
54
+ async function openBunFallback(dbPath, nodeFailure) {
55
+ if (!isBunRuntime())
56
+ return disableLearning(nodeFailure);
57
+ try {
58
+ const db = await openBunSqlite(dbPath);
59
+ sqliteAvailable = true;
60
+ driver = "bun:sqlite";
61
+ return db;
62
+ }
63
+ catch (err) {
64
+ if (err instanceof SqliteUnavailableError)
65
+ return disableLearning(err);
66
+ // bun:sqlite did load (this is Bun), so only this path failed, as on the node:sqlite path.
67
+ sqliteAvailable = true;
68
+ console.warn(`pi-warden: could not open ${dbPath}:`, err);
69
+ return NOOP_DB;
70
+ }
71
+ }
42
72
  /** Open one connection; a failure is remembered per path so later calls neither warn again nor retry forever. */
43
73
  async function openDb(dbPath) {
44
74
  let opened;
@@ -46,11 +76,8 @@ async function openDb(dbPath) {
46
76
  // Static import cannot work: node:sqlite is flagged experimental and loads lazily so a missing
47
77
  // or broken build of it disables learning features instead of failing the whole process.
48
78
  const sqlite = await import("node:sqlite").catch(err => ({ importFailed: err }));
49
- if ("importFailed" in sqlite) {
50
- sqliteAvailable = false;
51
- console.warn("pi-warden: node:sqlite unavailable, learning features disabled:", sqlite.importFailed);
52
- return NOOP_DB;
53
- }
79
+ if ("importFailed" in sqlite)
80
+ return await openBunFallback(dbPath, sqlite.importFailed);
54
81
  const { DatabaseSync } = sqlite;
55
82
  sqliteAvailable = true;
56
83
  // DatabaseSync does not create parent directories; on a fresh machine the folder may not exist yet.
@@ -58,6 +85,7 @@ async function openDb(dbPath) {
58
85
  opened = new DatabaseSync(dbPath);
59
86
  opened.exec("PRAGMA journal_mode = WAL");
60
87
  opened.exec("PRAGMA busy_timeout = 10000");
88
+ driver = "node:sqlite";
61
89
  return opened;
62
90
  }
63
91
  catch (err) {
@@ -82,6 +110,10 @@ async function getDb(dirs = defaultHostDirs()) {
82
110
  dbs.set(dbPath, opening = openDb(dbPath));
83
111
  return opening;
84
112
  }
113
+ /** Which module opened the learning database: "node:sqlite", "bun:sqlite", or undefined while learning is off. */
114
+ export function sqliteDriver() {
115
+ return driver;
116
+ }
85
117
  export async function initSchema(retentionDays = 365, dirs = defaultHostDirs()) {
86
118
  try {
87
119
  const d = await getDb(dirs);
@@ -0,0 +1,48 @@
1
+ /** The driver that opened the learning database. */
2
+ export type SqliteDriver = "node:sqlite" | "bun:sqlite";
3
+ /** Bindable values. Learning binds only positional `?` placeholders, never named keys, so the
4
+ * named-parameter prefixes never meet: Bun's default (non-strict) mode already accepts both a
5
+ * bare `name` and a `$name` key, and the adapter does not open in strict mode. */
6
+ export type SqliteValue = null | number | bigint | string | ArrayBufferView;
7
+ export type SqliteRow = Record<string, unknown>;
8
+ export interface SqliteRunResult {
9
+ changes: number | bigint;
10
+ lastInsertRowid: number | bigint;
11
+ }
12
+ export interface SqliteStatement {
13
+ run(...params: SqliteValue[]): SqliteRunResult;
14
+ get(...params: SqliteValue[]): SqliteRow | undefined;
15
+ all(...params: SqliteValue[]): SqliteRow[];
16
+ }
17
+ /** The DatabaseSync subset learning uses: schema and PRAGMAs through exec, rows through prepare. */
18
+ export interface SqliteDb {
19
+ exec(sql: string): void;
20
+ prepare(sql: string): SqliteStatement;
21
+ close(): void;
22
+ }
23
+ /** bun:sqlite's statement shape, as the adapter reads it. */
24
+ export interface BunSqliteStatement {
25
+ run(...params: unknown[]): SqliteRunResult;
26
+ get(...params: unknown[]): SqliteRow | null;
27
+ all(...params: unknown[]): SqliteRow[];
28
+ }
29
+ /** bun:sqlite's handle shape, as the adapter reads it. */
30
+ export interface BunSqliteHandle {
31
+ exec(sql: string): unknown;
32
+ prepare(sql: string): BunSqliteStatement;
33
+ close(): void;
34
+ }
35
+ /** bun:sqlite could not be loaded. The caller reports "neither module" and learning stays off. */
36
+ export declare class SqliteUnavailableError extends Error {
37
+ constructor(moduleName: string, cause: unknown);
38
+ }
39
+ /** True when this process is Bun, the only runtime where bun:sqlite can be loaded. */
40
+ export declare function isBunRuntime(): boolean;
41
+ /**
42
+ * Open the learning database through bun:sqlite. Throws SqliteUnavailableError when the module
43
+ * does not load; any other error is a path open or PRAGMA failure for this path only, and the
44
+ * half-open handle is closed first, as the node:sqlite path does.
45
+ */
46
+ export declare function openBunSqlite(dbPath: string): Promise<SqliteDb>;
47
+ /** Present a bun:sqlite handle as the DatabaseSync subset learning uses. */
48
+ export declare function wrapBunDatabase(db: BunSqliteHandle): SqliteDb;
@@ -0,0 +1,68 @@
1
+ // src/sqlite-adapter.ts - bun:sqlite fallback for the learning database
2
+ //
3
+ // node:sqlite stays the first choice and is not touched here. Pi's release binaries are Bun
4
+ // --compile executables in which node:sqlite may not be a built-in module, so learning needs a
5
+ // second way in: bun:sqlite, wrapped to answer like the DatabaseSync subset learning calls. One
6
+ // module owns the difference, so src/learning.ts keeps a single code path for both drivers.
7
+ import { mkdirSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ /** bun:sqlite could not be loaded. The caller reports "neither module" and learning stays off. */
10
+ export class SqliteUnavailableError extends Error {
11
+ constructor(moduleName, cause) {
12
+ super(`${moduleName} could not be loaded`, { cause });
13
+ }
14
+ }
15
+ /** True when this process is Bun, the only runtime where bun:sqlite can be loaded. */
16
+ export function isBunRuntime() {
17
+ return typeof process.versions.bun === "string" || typeof globalThis.Bun !== "undefined";
18
+ }
19
+ /**
20
+ * Open the learning database through bun:sqlite. Throws SqliteUnavailableError when the module
21
+ * does not load; any other error is a path open or PRAGMA failure for this path only, and the
22
+ * half-open handle is closed first, as the node:sqlite path does.
23
+ */
24
+ export async function openBunSqlite(dbPath) {
25
+ let bunSqlite;
26
+ try {
27
+ bunSqlite = await import("bun:sqlite");
28
+ }
29
+ catch (err) {
30
+ throw new SqliteUnavailableError("bun:sqlite", err);
31
+ }
32
+ // bun:sqlite does not create parent directories either; on a fresh machine the folder is missing.
33
+ mkdirSync(dirname(dbPath), { recursive: true, mode: 0o700 });
34
+ const db = wrapBunDatabase(new bunSqlite.Database(dbPath));
35
+ try {
36
+ db.exec("PRAGMA journal_mode = WAL");
37
+ db.exec("PRAGMA busy_timeout = 10000");
38
+ }
39
+ catch (err) {
40
+ try {
41
+ db.close();
42
+ }
43
+ catch { /* already closed */ }
44
+ throw err;
45
+ }
46
+ return db;
47
+ }
48
+ /** Present a bun:sqlite handle as the DatabaseSync subset learning uses. */
49
+ export function wrapBunDatabase(db) {
50
+ return {
51
+ exec(sql) {
52
+ db.exec(sql);
53
+ },
54
+ prepare(sql) {
55
+ const stmt = db.prepare(sql);
56
+ return {
57
+ run: (...params) => stmt.run(...params),
58
+ // Bun answers a miss with null where node:sqlite answers undefined. Learning reads no row
59
+ // as undefined, so the adapter hands back the node answer.
60
+ get: (...params) => stmt.get(...params) ?? undefined,
61
+ all: (...params) => stmt.all(...params),
62
+ };
63
+ },
64
+ close() {
65
+ db.close();
66
+ },
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-warden",
3
- "version": "0.74.0",
3
+ "version": "0.74.1",
4
4
  "description": "Makes the Pi agent follow your project's rules. Jev judges every write against your pi-warden.md and quotes the broken rule back to the agent, names slop, breaks stuck loops, calls out unverified done claims, compresses large tool output, and holds the rare destructive command. Built on pi-typesafe.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,6 +54,7 @@
54
54
  "build": "tsc -p tsconfig.build.json",
55
55
  "typecheck": "tsc --noEmit",
56
56
  "test": "node --import tsx --test tests/*.test.ts",
57
+ "test:bun": "bun test tests/sqlite-adapter.test.ts tests/learning.test.ts",
57
58
  "test:live": "node --env-file-if-exists=.env scripts/live-smoke.mjs",
58
59
  "dev:pi": "node scripts/dev-pi.mjs",
59
60
  "preview": "node scripts/render-preview.mjs",