Files
pi_harness/packages/storage/sqlite-node/src/index.ts
T
Cristina Poncela Cubeiro 9e7582aa03 feat: sqlite session storage (#6594)
This PR:

- Adds retainedTail to compaction entries in the new agent harness so we don't have to walk up the tree for the 2000 tokens before compaction,
- Changes getPathToRoot to getPathToRootOrCompaction to only load until last compaction, as unnecessary to access all nodes where it is called,
- Adds a SQLite storage backend, in a separate packages/session-backend-sqlite, with a migration system and schemas as per on-site discussions: sessions to match session header messages (except for metadata, which I couldn't understand what it's used for or where it gets written, so I omitted it), session_entries for shared entry types as columns plus payload as a json for what remains, session_sequences to represent the append-only, serialized nature of the jsonl files, branch_entries to attribute nodes to branches (relationship one-to-many), and session_materialized with the session info (see /session in TUI) to act as a "cache" or quick-access for costs, message count, token info, labels, session name, and model-thinking-level config (e.g. for fast resume).
- This is compatible with the new agent harness Session abstraction.
2026-07-21 11:36:31 +02:00

98 lines
2.8 KiB
TypeScript

import type { SQLInputValue } from "node:sqlite";
import { DatabaseSync } from "node:sqlite";
import type { SqliteDatabase, SqliteDatabaseFactory, SqliteRunResult, SqliteStatement } from "./sqlite/types.ts";
function isNamedParameters(value: unknown): value is Record<string, SQLInputValue> {
if (value === null || typeof value !== "object") return false;
if (Array.isArray(value) || ArrayBuffer.isView(value)) return false;
return true;
}
class NodeSqliteStatement implements SqliteStatement {
private readonly statement: ReturnType<DatabaseSync["prepare"]>;
constructor(statement: ReturnType<DatabaseSync["prepare"]>) {
this.statement = statement;
}
async run(...params: unknown[]): Promise<SqliteRunResult> {
const [first, ...rest] = params;
const result = isNamedParameters(first)
? this.statement.run(first, ...(rest as SQLInputValue[]))
: this.statement.run(...(params as SQLInputValue[]));
return {
changes: Number(result.changes),
lastInsertRowid: result.lastInsertRowid === undefined ? undefined : Number(result.lastInsertRowid),
};
}
async get<TRow extends object>(...params: unknown[]): Promise<TRow | undefined> {
const [first, ...rest] = params;
return (
isNamedParameters(first)
? this.statement.get(first, ...(rest as SQLInputValue[]))
: this.statement.get(...(params as SQLInputValue[]))
) as TRow | undefined;
}
async all<TRow extends object>(...params: unknown[]): Promise<TRow[]> {
const [first, ...rest] = params;
return (
isNamedParameters(first)
? this.statement.all(first, ...(rest as SQLInputValue[]))
: this.statement.all(...(params as SQLInputValue[]))
) as TRow[];
}
}
class NodeSqliteDatabase implements SqliteDatabase {
private readonly db: DatabaseSync;
constructor(db: DatabaseSync) {
this.db = db;
}
async exec(sql: string): Promise<void> {
this.db.exec(sql);
}
prepare(sql: string): SqliteStatement {
return new NodeSqliteStatement(this.db.prepare(sql));
}
async transaction<T>(fn: () => Promise<T>): Promise<T> {
this.db.exec("BEGIN");
try {
const result = await fn();
this.db.exec("COMMIT");
return result;
} catch (error) {
try {
this.db.exec("ROLLBACK");
} catch {
// Ignore rollback errors to rethrow original error.
}
throw error;
}
}
async close(): Promise<void> {
this.db.close();
}
}
export function wrapNodeSqliteDatabase(db: DatabaseSync): SqliteDatabase {
return new NodeSqliteDatabase(db);
}
export function createNodeSqliteFactory(): SqliteDatabaseFactory {
return {
async open(path: string): Promise<SqliteDatabase> {
return new NodeSqliteDatabase(new DatabaseSync(path));
},
};
}
// Re-export the SQLite session storage backend and types so this package is a complete node-sqlite backend.
export * from "./sqlite/index.ts";