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.
This commit is contained in:
committed by
GitHub
parent
54fad505b9
commit
9e7582aa03
@@ -0,0 +1,97 @@
|
||||
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";
|
||||
Reference in New Issue
Block a user