@earendil-works/pi-session-backend-sqlite-node 0.84.0
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 +32 -0
- package/README.md +15 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/sqlite/branch-cache.d.ts +6 -0
- package/dist/sqlite/branch-cache.d.ts.map +1 -0
- package/dist/sqlite/branch-cache.js +71 -0
- package/dist/sqlite/branch-cache.js.map +1 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +4 -0
- package/dist/sqlite/index.js.map +1 -0
- package/dist/sqlite/migrations/001_initial.sql +123 -0
- package/dist/sqlite/migrations.d.ts +9 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/migrations.js +38 -0
- package/dist/sqlite/migrations.js.map +1 -0
- package/dist/sqlite/repo.d.ts +41 -0
- package/dist/sqlite/repo.d.ts.map +1 -0
- package/dist/sqlite/repo.js +736 -0
- package/dist/sqlite/repo.js.map +1 -0
- package/dist/sqlite/search-backend.d.ts +9 -0
- package/dist/sqlite/search-backend.d.ts.map +1 -0
- package/dist/sqlite/search-backend.js +118 -0
- package/dist/sqlite/search-backend.js.map +1 -0
- package/dist/sqlite/storage/branch-entries.d.ts +35 -0
- package/dist/sqlite/storage/branch-entries.d.ts.map +1 -0
- package/dist/sqlite/storage/branch-entries.js +86 -0
- package/dist/sqlite/storage/branch-entries.js.map +1 -0
- package/dist/sqlite/storage/branch-tips.d.ts +7 -0
- package/dist/sqlite/storage/branch-tips.d.ts.map +1 -0
- package/dist/sqlite/storage/branch-tips.js +25 -0
- package/dist/sqlite/storage/branch-tips.js.map +1 -0
- package/dist/sqlite/storage/entries.d.ts +29 -0
- package/dist/sqlite/storage/entries.d.ts.map +1 -0
- package/dist/sqlite/storage/entries.js +38 -0
- package/dist/sqlite/storage/entries.js.map +1 -0
- package/dist/sqlite/storage/facts.d.ts +19 -0
- package/dist/sqlite/storage/facts.d.ts.map +1 -0
- package/dist/sqlite/storage/facts.js +41 -0
- package/dist/sqlite/storage/facts.js.map +1 -0
- package/dist/sqlite/storage/lanes.d.ts +34 -0
- package/dist/sqlite/storage/lanes.d.ts.map +1 -0
- package/dist/sqlite/storage/lanes.js +106 -0
- package/dist/sqlite/storage/lanes.js.map +1 -0
- package/dist/sqlite/storage/records.d.ts +38 -0
- package/dist/sqlite/storage/records.d.ts.map +1 -0
- package/dist/sqlite/storage/records.js +69 -0
- package/dist/sqlite/storage/records.js.map +1 -0
- package/dist/sqlite/storage/session-sequences.d.ts +7 -0
- package/dist/sqlite/storage/session-sequences.d.ts.map +1 -0
- package/dist/sqlite/storage/session-sequences.js +23 -0
- package/dist/sqlite/storage/session-sequences.js.map +1 -0
- package/dist/sqlite/storage/session-stats.d.ts +17 -0
- package/dist/sqlite/storage/session-stats.d.ts.map +1 -0
- package/dist/sqlite/storage/session-stats.js +45 -0
- package/dist/sqlite/storage/session-stats.js.map +1 -0
- package/dist/sqlite/storage/sessions.d.ts +26 -0
- package/dist/sqlite/storage/sessions.d.ts.map +1 -0
- package/dist/sqlite/storage/sessions.js +102 -0
- package/dist/sqlite/storage/sessions.js.map +1 -0
- package/dist/sqlite/storage/writer-leases.d.ts +15 -0
- package/dist/sqlite/storage/writer-leases.d.ts.map +1 -0
- package/dist/sqlite/storage/writer-leases.js +30 -0
- package/dist/sqlite/storage/writer-leases.js.map +1 -0
- package/dist/sqlite/types.d.ts +44 -0
- package/dist/sqlite/types.d.ts.map +1 -0
- package/dist/sqlite/types.js +2 -0
- package/dist/sqlite/types.js.map +1 -0
- package/package.json +44 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.84.0] - 2026-08-06
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed the package from `@earendil-works/pi-storage-sqlite-node` to `@earendil-works/pi-session-backend-sqlite-node`.
|
|
10
|
+
- Replaced the legacy SQLite session schema and repository with the v4 lane-based `SessionRepo` contract. Existing work-in-progress databases are not migrated.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added bounded active-branch queries, durable operation records, global facts, shared sequence allocation, session statistics, and fenced writer leases to the SQLite backend.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed SQLite session listings to avoid acquiring writer claims and include current session names, allowing inventory reads while sessions have active writers ([#7655](https://github.com/earendil-works/pi/pull/7655)).
|
|
19
|
+
|
|
20
|
+
## [0.83.0] - 2026-07-29
|
|
21
|
+
|
|
22
|
+
## [0.82.1] - 2026-07-25
|
|
23
|
+
|
|
24
|
+
## [0.82.0] - 2026-07-24
|
|
25
|
+
|
|
26
|
+
## [0.81.1] - 2026-07-21
|
|
27
|
+
|
|
28
|
+
## [0.81.0] - 2026-07-21
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added a Node.js SQLite storage backend for agent harness sessions, including migrations and materialized session views ([#6594](https://github.com/earendil-works/pi/pull/6594) by [@cristinaponcela](https://github.com/cristinaponcela)).
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @earendil-works/pi-session-backend-sqlite-node
|
|
2
|
+
|
|
3
|
+
Node sqlite session backend for `@earendil-works/pi-agent-core` sessions. Provides the
|
|
4
|
+
`node:sqlite` adapter (`SqliteDatabase` implementation), SQLite session repository,
|
|
5
|
+
migrations, materialized views, and optional FTS search.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
await using repository = new SqliteSessionRepository(options);
|
|
9
|
+
const search = createSqliteSessionSearch(options);
|
|
10
|
+
const session = await repository.create({ cwd });
|
|
11
|
+
const hits = await search.search({ text: "needle" });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The repository lazily owns one shared database connection. Search is an independent,
|
|
15
|
+
query-only projection over the same canonical database.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { SqliteDatabase, SqliteDatabaseFactory } from "./sqlite/types.ts";
|
|
3
|
+
export declare function wrapNodeSqliteDatabase(db: DatabaseSync): SqliteDatabase;
|
|
4
|
+
export declare function createNodeSqliteFactory(): SqliteDatabaseFactory;
|
|
5
|
+
export * from "./sqlite/index.ts";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAoC,MAAM,mBAAmB,CAAC;AAwFjH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,YAAY,GAAG,cAAc,CAEvE;AAED,wBAAgB,uBAAuB,IAAI,qBAAqB,CAM/D;AAGD,cAAc,mBAAmB,CAAC","sourcesContent":["import type { SQLInputValue } from \"node:sqlite\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SqliteDatabase, SqliteDatabaseFactory, SqliteRunResult, SqliteStatement } from \"./sqlite/types.ts\";\n\nfunction isNamedParameters(value: unknown): value is Record<string, SQLInputValue> {\n\tif (value === null || typeof value !== \"object\") return false;\n\tif (Array.isArray(value) || ArrayBuffer.isView(value)) return false;\n\treturn true;\n}\n\nfunction isAsyncResult(value: unknown): boolean {\n\treturn value !== null && (typeof value === \"object\" || typeof value === \"function\") && \"then\" in value;\n}\n\nclass NodeSqliteStatement implements SqliteStatement {\n\tprivate readonly statement: ReturnType<DatabaseSync[\"prepare\"]>;\n\n\tconstructor(statement: ReturnType<DatabaseSync[\"prepare\"]>) {\n\t\tthis.statement = statement;\n\t}\n\n\trun(...params: unknown[]): SqliteRunResult {\n\t\tconst [first, ...rest] = params;\n\t\tconst result = isNamedParameters(first)\n\t\t\t? this.statement.run(first, ...(rest as SQLInputValue[]))\n\t\t\t: this.statement.run(...(params as SQLInputValue[]));\n\t\treturn {\n\t\t\tchanges: Number(result.changes),\n\t\t\tlastInsertRowid: result.lastInsertRowid === undefined ? undefined : Number(result.lastInsertRowid),\n\t\t};\n\t}\n\n\tget<TRow extends object>(...params: unknown[]): TRow | undefined {\n\t\tconst [first, ...rest] = params;\n\t\treturn (\n\t\t\tisNamedParameters(first)\n\t\t\t\t? this.statement.get(first, ...(rest as SQLInputValue[]))\n\t\t\t\t: this.statement.get(...(params as SQLInputValue[]))\n\t\t) as TRow | undefined;\n\t}\n\n\tall<TRow extends object>(...params: unknown[]): TRow[] {\n\t\tconst [first, ...rest] = params;\n\t\treturn (\n\t\t\tisNamedParameters(first)\n\t\t\t\t? this.statement.all(first, ...(rest as SQLInputValue[]))\n\t\t\t\t: this.statement.all(...(params as SQLInputValue[]))\n\t\t) as TRow[];\n\t}\n}\n\nclass NodeSqliteDatabase implements SqliteDatabase {\n\tprivate readonly db: DatabaseSync;\n\n\tconstructor(db: DatabaseSync) {\n\t\tthis.db = db;\n\t}\n\n\texec(sql: string): void {\n\t\tthis.db.exec(sql);\n\t}\n\n\tprepare(sql: string): SqliteStatement {\n\t\treturn new NodeSqliteStatement(this.db.prepare(sql));\n\t}\n\n\ttransaction<T>(fn: () => T): T {\n\t\tthis.db.exec(\"BEGIN IMMEDIATE\");\n\t\ttry {\n\t\t\tconst result = fn();\n\t\t\tif (isAsyncResult(result)) {\n\t\t\t\tthrow new TypeError(\"SQLite transaction callbacks must be synchronous\");\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\");\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tthis.db.exec(\"ROLLBACK\");\n\t\t\t} catch {\n\t\t\t\t// Ignore rollback errors to rethrow original error.\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n\nexport function wrapNodeSqliteDatabase(db: DatabaseSync): SqliteDatabase {\n\treturn new NodeSqliteDatabase(db);\n}\n\nexport function createNodeSqliteFactory(): SqliteDatabaseFactory {\n\treturn {\n\t\tasync open(path: string): Promise<SqliteDatabase> {\n\t\t\treturn new NodeSqliteDatabase(new DatabaseSync(path));\n\t\t},\n\t};\n}\n\n// Re-export the SQLite session backend and types so this package is a complete node-sqlite backend.\nexport * from \"./sqlite/index.ts\";\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
function isNamedParameters(value) {
|
|
3
|
+
if (value === null || typeof value !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
if (Array.isArray(value) || ArrayBuffer.isView(value))
|
|
6
|
+
return false;
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
function isAsyncResult(value) {
|
|
10
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && "then" in value;
|
|
11
|
+
}
|
|
12
|
+
class NodeSqliteStatement {
|
|
13
|
+
statement;
|
|
14
|
+
constructor(statement) {
|
|
15
|
+
this.statement = statement;
|
|
16
|
+
}
|
|
17
|
+
run(...params) {
|
|
18
|
+
const [first, ...rest] = params;
|
|
19
|
+
const result = isNamedParameters(first)
|
|
20
|
+
? this.statement.run(first, ...rest)
|
|
21
|
+
: this.statement.run(...params);
|
|
22
|
+
return {
|
|
23
|
+
changes: Number(result.changes),
|
|
24
|
+
lastInsertRowid: result.lastInsertRowid === undefined ? undefined : Number(result.lastInsertRowid),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
get(...params) {
|
|
28
|
+
const [first, ...rest] = params;
|
|
29
|
+
return (isNamedParameters(first)
|
|
30
|
+
? this.statement.get(first, ...rest)
|
|
31
|
+
: this.statement.get(...params));
|
|
32
|
+
}
|
|
33
|
+
all(...params) {
|
|
34
|
+
const [first, ...rest] = params;
|
|
35
|
+
return (isNamedParameters(first)
|
|
36
|
+
? this.statement.all(first, ...rest)
|
|
37
|
+
: this.statement.all(...params));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class NodeSqliteDatabase {
|
|
41
|
+
db;
|
|
42
|
+
constructor(db) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
}
|
|
45
|
+
exec(sql) {
|
|
46
|
+
this.db.exec(sql);
|
|
47
|
+
}
|
|
48
|
+
prepare(sql) {
|
|
49
|
+
return new NodeSqliteStatement(this.db.prepare(sql));
|
|
50
|
+
}
|
|
51
|
+
transaction(fn) {
|
|
52
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
53
|
+
try {
|
|
54
|
+
const result = fn();
|
|
55
|
+
if (isAsyncResult(result)) {
|
|
56
|
+
throw new TypeError("SQLite transaction callbacks must be synchronous");
|
|
57
|
+
}
|
|
58
|
+
this.db.exec("COMMIT");
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
try {
|
|
63
|
+
this.db.exec("ROLLBACK");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore rollback errors to rethrow original error.
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
close() {
|
|
72
|
+
this.db.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function wrapNodeSqliteDatabase(db) {
|
|
76
|
+
return new NodeSqliteDatabase(db);
|
|
77
|
+
}
|
|
78
|
+
export function createNodeSqliteFactory() {
|
|
79
|
+
return {
|
|
80
|
+
async open(path) {
|
|
81
|
+
return new NodeSqliteDatabase(new DatabaseSync(path));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Re-export the SQLite session backend and types so this package is a complete node-sqlite backend.
|
|
86
|
+
export * from "./sqlite/index.js";
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SQLInputValue } from \"node:sqlite\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SqliteDatabase, SqliteDatabaseFactory, SqliteRunResult, SqliteStatement } from \"./sqlite/types.ts\";\n\nfunction isNamedParameters(value: unknown): value is Record<string, SQLInputValue> {\n\tif (value === null || typeof value !== \"object\") return false;\n\tif (Array.isArray(value) || ArrayBuffer.isView(value)) return false;\n\treturn true;\n}\n\nfunction isAsyncResult(value: unknown): boolean {\n\treturn value !== null && (typeof value === \"object\" || typeof value === \"function\") && \"then\" in value;\n}\n\nclass NodeSqliteStatement implements SqliteStatement {\n\tprivate readonly statement: ReturnType<DatabaseSync[\"prepare\"]>;\n\n\tconstructor(statement: ReturnType<DatabaseSync[\"prepare\"]>) {\n\t\tthis.statement = statement;\n\t}\n\n\trun(...params: unknown[]): SqliteRunResult {\n\t\tconst [first, ...rest] = params;\n\t\tconst result = isNamedParameters(first)\n\t\t\t? this.statement.run(first, ...(rest as SQLInputValue[]))\n\t\t\t: this.statement.run(...(params as SQLInputValue[]));\n\t\treturn {\n\t\t\tchanges: Number(result.changes),\n\t\t\tlastInsertRowid: result.lastInsertRowid === undefined ? undefined : Number(result.lastInsertRowid),\n\t\t};\n\t}\n\n\tget<TRow extends object>(...params: unknown[]): TRow | undefined {\n\t\tconst [first, ...rest] = params;\n\t\treturn (\n\t\t\tisNamedParameters(first)\n\t\t\t\t? this.statement.get(first, ...(rest as SQLInputValue[]))\n\t\t\t\t: this.statement.get(...(params as SQLInputValue[]))\n\t\t) as TRow | undefined;\n\t}\n\n\tall<TRow extends object>(...params: unknown[]): TRow[] {\n\t\tconst [first, ...rest] = params;\n\t\treturn (\n\t\t\tisNamedParameters(first)\n\t\t\t\t? this.statement.all(first, ...(rest as SQLInputValue[]))\n\t\t\t\t: this.statement.all(...(params as SQLInputValue[]))\n\t\t) as TRow[];\n\t}\n}\n\nclass NodeSqliteDatabase implements SqliteDatabase {\n\tprivate readonly db: DatabaseSync;\n\n\tconstructor(db: DatabaseSync) {\n\t\tthis.db = db;\n\t}\n\n\texec(sql: string): void {\n\t\tthis.db.exec(sql);\n\t}\n\n\tprepare(sql: string): SqliteStatement {\n\t\treturn new NodeSqliteStatement(this.db.prepare(sql));\n\t}\n\n\ttransaction<T>(fn: () => T): T {\n\t\tthis.db.exec(\"BEGIN IMMEDIATE\");\n\t\ttry {\n\t\t\tconst result = fn();\n\t\t\tif (isAsyncResult(result)) {\n\t\t\t\tthrow new TypeError(\"SQLite transaction callbacks must be synchronous\");\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\");\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tthis.db.exec(\"ROLLBACK\");\n\t\t\t} catch {\n\t\t\t\t// Ignore rollback errors to rethrow original error.\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n\nexport function wrapNodeSqliteDatabase(db: DatabaseSync): SqliteDatabase {\n\treturn new NodeSqliteDatabase(db);\n}\n\nexport function createNodeSqliteFactory(): SqliteDatabaseFactory {\n\treturn {\n\t\tasync open(path: string): Promise<SqliteDatabase> {\n\t\t\treturn new NodeSqliteDatabase(new DatabaseSync(path));\n\t\t},\n\t};\n}\n\n// Re-export the SQLite session backend and types so this package is a complete node-sqlite backend.\nexport * from \"./sqlite/index.ts\";\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SqliteDatabase } from "./types.ts";
|
|
2
|
+
export declare function deleteBranchCache(db: SqliteDatabase, sessionId: string): void;
|
|
3
|
+
export declare function rebuildBranchCache(db: SqliteDatabase, sessionId: string): void;
|
|
4
|
+
export declare function buildCachedBranch(db: SqliteDatabase, sessionId: string, leafId: string): void;
|
|
5
|
+
export declare function appendEntryToBranchCache(db: SqliteDatabase, sessionId: string, entryId: string, entrySeq: number, entryType: string, customType: string | null, parentId: string | null): void;
|
|
6
|
+
//# sourceMappingURL=branch-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-cache.d.ts","sourceRoot":"","sources":["../../src/sqlite/branch-cache.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,QAGtE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,QAcvE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAqBtF;AAkBD,wBAAgB,wBAAwB,CACvC,EAAE,EAAE,cAAc,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,QAwBvB","sourcesContent":["import { SessionError } from \"@earendil-works/pi-agent-core\";\nimport { uuidv7 } from \"@earendil-works/pi-ai\";\nimport {\n\tcopyBranchEntriesThroughSeq,\n\tdeleteBranchEntries,\n\tinsertBranchEntriesForPath,\n\tinsertBranchEntry,\n\treadBranchContainingEntry,\n} from \"./storage/branch-entries.ts\";\n\nimport { deleteBranchTips, insertBranchTip, readBranchTipBranchId, updateBranchTip } from \"./storage/branch-tips.ts\";\nimport type { SqliteDatabase } from \"./types.ts\";\n\nexport function deleteBranchCache(db: SqliteDatabase, sessionId: string) {\n\tdeleteBranchTips(db, sessionId);\n\tdeleteBranchEntries(db, sessionId);\n}\n\nexport function rebuildBranchCache(db: SqliteDatabase, sessionId: string) {\n\tconst tips = db\n\t\t.prepare(\n\t\t\t`SELECT leaf.id\n\t\t\tFROM entries AS leaf\n\t\t\tWHERE leaf.session_id = ?\n\t\t\t\tAND NOT EXISTS (\n\t\t\t\t\tSELECT 1 FROM entries AS child WHERE child.session_id = leaf.session_id AND child.parent_id = leaf.id\n\t\t\t\t)\n\t\t\tORDER BY leaf.seq`,\n\t\t)\n\t\t.all<{ id: string }>(sessionId);\n\tdeleteBranchCache(db, sessionId);\n\tfor (const tip of tips) buildCachedBranch(db, sessionId, tip.id);\n}\n\nexport function buildCachedBranch(db: SqliteDatabase, sessionId: string, leafId: string) {\n\tdb.exec(\"SAVEPOINT build_branch_cache\");\n\ttry {\n\t\tconst branchId = uuidv7();\n\t\tinsertBranchEntriesForPath(db, sessionId, branchId, leafId);\n\t\tinsertBranchTip(db, sessionId, leafId, branchId);\n\t\tdb.exec(\"RELEASE SAVEPOINT build_branch_cache\");\n\t} catch (error) {\n\t\ttry {\n\t\t\tdb.exec(\"ROLLBACK TO SAVEPOINT build_branch_cache\");\n\t\t\tdb.exec(\"RELEASE SAVEPOINT build_branch_cache\");\n\t\t} catch {\n\t\t\t// Preserve the original build failure.\n\t\t}\n\t\tif (error instanceof SessionError) throw error;\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Failed to build SQLite branch cache at entry ${leafId}`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n}\n\nfunction extendBranch(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tbranchId: string,\n\tparentId: string,\n\tentryId: string,\n\tentrySeq: number,\n\tentryType: string,\n\tcustomType: string | null,\n) {\n\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\tif (!updateBranchTip(db, sessionId, branchId, parentId, entryId)) {\n\t\tthrow new SessionError(\"invalid_entry\", `Branch tip ${parentId} changed during append`);\n\t}\n}\n\nexport function appendEntryToBranchCache(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tentryId: string,\n\tentrySeq: number,\n\tentryType: string,\n\tcustomType: string | null,\n\tparentId: string | null,\n) {\n\tif (parentId === null) {\n\t\tconst branchId = uuidv7();\n\t\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\t\tinsertBranchTip(db, sessionId, entryId, branchId);\n\t\treturn;\n\t}\n\n\tconst tipBranchId = readBranchTipBranchId(db, sessionId, parentId);\n\tif (tipBranchId !== undefined) {\n\t\textendBranch(db, sessionId, tipBranchId, parentId, entryId, entrySeq, entryType, customType);\n\t\treturn;\n\t}\n\n\tconst source = readBranchContainingEntry(db, sessionId, parentId);\n\tif (!source) {\n\t\tthrow new SessionError(\"invalid_entry\", `Branch cache has no branch containing parent entry ${parentId}`);\n\t}\n\n\tconst branchId = uuidv7();\n\tcopyBranchEntriesThroughSeq(db, sessionId, branchId, source.branchId, source.entrySeq);\n\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\tinsertBranchTip(db, sessionId, entryId, branchId);\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SessionError } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
3
|
+
import { copyBranchEntriesThroughSeq, deleteBranchEntries, insertBranchEntriesForPath, insertBranchEntry, readBranchContainingEntry, } from "./storage/branch-entries.js";
|
|
4
|
+
import { deleteBranchTips, insertBranchTip, readBranchTipBranchId, updateBranchTip } from "./storage/branch-tips.js";
|
|
5
|
+
export function deleteBranchCache(db, sessionId) {
|
|
6
|
+
deleteBranchTips(db, sessionId);
|
|
7
|
+
deleteBranchEntries(db, sessionId);
|
|
8
|
+
}
|
|
9
|
+
export function rebuildBranchCache(db, sessionId) {
|
|
10
|
+
const tips = db
|
|
11
|
+
.prepare(`SELECT leaf.id
|
|
12
|
+
FROM entries AS leaf
|
|
13
|
+
WHERE leaf.session_id = ?
|
|
14
|
+
AND NOT EXISTS (
|
|
15
|
+
SELECT 1 FROM entries AS child WHERE child.session_id = leaf.session_id AND child.parent_id = leaf.id
|
|
16
|
+
)
|
|
17
|
+
ORDER BY leaf.seq`)
|
|
18
|
+
.all(sessionId);
|
|
19
|
+
deleteBranchCache(db, sessionId);
|
|
20
|
+
for (const tip of tips)
|
|
21
|
+
buildCachedBranch(db, sessionId, tip.id);
|
|
22
|
+
}
|
|
23
|
+
export function buildCachedBranch(db, sessionId, leafId) {
|
|
24
|
+
db.exec("SAVEPOINT build_branch_cache");
|
|
25
|
+
try {
|
|
26
|
+
const branchId = uuidv7();
|
|
27
|
+
insertBranchEntriesForPath(db, sessionId, branchId, leafId);
|
|
28
|
+
insertBranchTip(db, sessionId, leafId, branchId);
|
|
29
|
+
db.exec("RELEASE SAVEPOINT build_branch_cache");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
try {
|
|
33
|
+
db.exec("ROLLBACK TO SAVEPOINT build_branch_cache");
|
|
34
|
+
db.exec("RELEASE SAVEPOINT build_branch_cache");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Preserve the original build failure.
|
|
38
|
+
}
|
|
39
|
+
if (error instanceof SessionError)
|
|
40
|
+
throw error;
|
|
41
|
+
throw new SessionError("storage", `Failed to build SQLite branch cache at entry ${leafId}`, error instanceof Error ? error : undefined);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function extendBranch(db, sessionId, branchId, parentId, entryId, entrySeq, entryType, customType) {
|
|
45
|
+
insertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);
|
|
46
|
+
if (!updateBranchTip(db, sessionId, branchId, parentId, entryId)) {
|
|
47
|
+
throw new SessionError("invalid_entry", `Branch tip ${parentId} changed during append`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function appendEntryToBranchCache(db, sessionId, entryId, entrySeq, entryType, customType, parentId) {
|
|
51
|
+
if (parentId === null) {
|
|
52
|
+
const branchId = uuidv7();
|
|
53
|
+
insertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);
|
|
54
|
+
insertBranchTip(db, sessionId, entryId, branchId);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const tipBranchId = readBranchTipBranchId(db, sessionId, parentId);
|
|
58
|
+
if (tipBranchId !== undefined) {
|
|
59
|
+
extendBranch(db, sessionId, tipBranchId, parentId, entryId, entrySeq, entryType, customType);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const source = readBranchContainingEntry(db, sessionId, parentId);
|
|
63
|
+
if (!source) {
|
|
64
|
+
throw new SessionError("invalid_entry", `Branch cache has no branch containing parent entry ${parentId}`);
|
|
65
|
+
}
|
|
66
|
+
const branchId = uuidv7();
|
|
67
|
+
copyBranchEntriesThroughSeq(db, sessionId, branchId, source.branchId, source.entrySeq);
|
|
68
|
+
insertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);
|
|
69
|
+
insertBranchTip(db, sessionId, entryId, branchId);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=branch-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-cache.js","sourceRoot":"","sources":["../../src/sqlite/branch-cache.ts"],"names":[],"mappings":"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","sourcesContent":["import { SessionError } from \"@earendil-works/pi-agent-core\";\nimport { uuidv7 } from \"@earendil-works/pi-ai\";\nimport {\n\tcopyBranchEntriesThroughSeq,\n\tdeleteBranchEntries,\n\tinsertBranchEntriesForPath,\n\tinsertBranchEntry,\n\treadBranchContainingEntry,\n} from \"./storage/branch-entries.ts\";\n\nimport { deleteBranchTips, insertBranchTip, readBranchTipBranchId, updateBranchTip } from \"./storage/branch-tips.ts\";\nimport type { SqliteDatabase } from \"./types.ts\";\n\nexport function deleteBranchCache(db: SqliteDatabase, sessionId: string) {\n\tdeleteBranchTips(db, sessionId);\n\tdeleteBranchEntries(db, sessionId);\n}\n\nexport function rebuildBranchCache(db: SqliteDatabase, sessionId: string) {\n\tconst tips = db\n\t\t.prepare(\n\t\t\t`SELECT leaf.id\n\t\t\tFROM entries AS leaf\n\t\t\tWHERE leaf.session_id = ?\n\t\t\t\tAND NOT EXISTS (\n\t\t\t\t\tSELECT 1 FROM entries AS child WHERE child.session_id = leaf.session_id AND child.parent_id = leaf.id\n\t\t\t\t)\n\t\t\tORDER BY leaf.seq`,\n\t\t)\n\t\t.all<{ id: string }>(sessionId);\n\tdeleteBranchCache(db, sessionId);\n\tfor (const tip of tips) buildCachedBranch(db, sessionId, tip.id);\n}\n\nexport function buildCachedBranch(db: SqliteDatabase, sessionId: string, leafId: string) {\n\tdb.exec(\"SAVEPOINT build_branch_cache\");\n\ttry {\n\t\tconst branchId = uuidv7();\n\t\tinsertBranchEntriesForPath(db, sessionId, branchId, leafId);\n\t\tinsertBranchTip(db, sessionId, leafId, branchId);\n\t\tdb.exec(\"RELEASE SAVEPOINT build_branch_cache\");\n\t} catch (error) {\n\t\ttry {\n\t\t\tdb.exec(\"ROLLBACK TO SAVEPOINT build_branch_cache\");\n\t\t\tdb.exec(\"RELEASE SAVEPOINT build_branch_cache\");\n\t\t} catch {\n\t\t\t// Preserve the original build failure.\n\t\t}\n\t\tif (error instanceof SessionError) throw error;\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Failed to build SQLite branch cache at entry ${leafId}`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n}\n\nfunction extendBranch(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tbranchId: string,\n\tparentId: string,\n\tentryId: string,\n\tentrySeq: number,\n\tentryType: string,\n\tcustomType: string | null,\n) {\n\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\tif (!updateBranchTip(db, sessionId, branchId, parentId, entryId)) {\n\t\tthrow new SessionError(\"invalid_entry\", `Branch tip ${parentId} changed during append`);\n\t}\n}\n\nexport function appendEntryToBranchCache(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tentryId: string,\n\tentrySeq: number,\n\tentryType: string,\n\tcustomType: string | null,\n\tparentId: string | null,\n) {\n\tif (parentId === null) {\n\t\tconst branchId = uuidv7();\n\t\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\t\tinsertBranchTip(db, sessionId, entryId, branchId);\n\t\treturn;\n\t}\n\n\tconst tipBranchId = readBranchTipBranchId(db, sessionId, parentId);\n\tif (tipBranchId !== undefined) {\n\t\textendBranch(db, sessionId, tipBranchId, parentId, entryId, entrySeq, entryType, customType);\n\t\treturn;\n\t}\n\n\tconst source = readBranchContainingEntry(db, sessionId, parentId);\n\tif (!source) {\n\t\tthrow new SessionError(\"invalid_entry\", `Branch cache has no branch containing parent entry ${parentId}`);\n\t}\n\n\tconst branchId = uuidv7();\n\tcopyBranchEntriesThroughSeq(db, sessionId, branchId, source.branchId, source.entrySeq);\n\tinsertBranchEntry(db, sessionId, branchId, entryId, entrySeq, entryType, customType);\n\tinsertBranchTip(db, sessionId, entryId, branchId);\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./migrations.ts";
|
|
2
|
+
export { SqliteSessionRepository, type SqliteSessionRepositoryOptions, type SqliteWriterLeaseOptions, } from "./repo.ts";
|
|
3
|
+
export * from "./search-backend.ts";
|
|
4
|
+
export type { SqliteDatabase, SqliteDatabaseFactory, SqliteRunResult, SqliteSessionCreateOptions, SqliteSessionListOptions, SqliteSessionMetadata, SqliteSessionRepositoryEnv, SqliteStatement, } from "./types.ts";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sqlite/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,OAAO,EACN,uBAAuB,EACvB,KAAK,8BAA8B,EACnC,KAAK,wBAAwB,GAC7B,MAAM,WAAW,CAAC;AACnB,cAAc,qBAAqB,CAAC;AACpC,YAAY,EACX,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,EAC1B,eAAe,GACf,MAAM,YAAY,CAAC","sourcesContent":["export * from \"./migrations.ts\";\nexport {\n\tSqliteSessionRepository,\n\ttype SqliteSessionRepositoryOptions,\n\ttype SqliteWriterLeaseOptions,\n} from \"./repo.ts\";\nexport * from \"./search-backend.ts\";\nexport type {\n\tSqliteDatabase,\n\tSqliteDatabaseFactory,\n\tSqliteRunResult,\n\tSqliteSessionCreateOptions,\n\tSqliteSessionListOptions,\n\tSqliteSessionMetadata,\n\tSqliteSessionRepositoryEnv,\n\tSqliteStatement,\n} from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sqlite/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,OAAO,EACN,uBAAuB,GAGvB,MAAM,WAAW,CAAC;AACnB,cAAc,qBAAqB,CAAC","sourcesContent":["export * from \"./migrations.ts\";\nexport {\n\tSqliteSessionRepository,\n\ttype SqliteSessionRepositoryOptions,\n\ttype SqliteWriterLeaseOptions,\n} from \"./repo.ts\";\nexport * from \"./search-backend.ts\";\nexport type {\n\tSqliteDatabase,\n\tSqliteDatabaseFactory,\n\tSqliteRunResult,\n\tSqliteSessionCreateOptions,\n\tSqliteSessionListOptions,\n\tSqliteSessionMetadata,\n\tSqliteSessionRepositoryEnv,\n\tSqliteStatement,\n} from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
created_at TEXT NOT NULL,
|
|
4
|
+
cwd TEXT NOT NULL,
|
|
5
|
+
parent_session_id TEXT NULL,
|
|
6
|
+
metadata TEXT NULL
|
|
7
|
+
) WITHOUT ROWID;
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at DESC);
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
14
|
+
session_id TEXT NOT NULL,
|
|
15
|
+
seq INTEGER NOT NULL,
|
|
16
|
+
id TEXT NOT NULL,
|
|
17
|
+
parent_id TEXT NULL,
|
|
18
|
+
type TEXT NOT NULL,
|
|
19
|
+
timestamp TEXT NOT NULL,
|
|
20
|
+
payload TEXT NOT NULL,
|
|
21
|
+
PRIMARY KEY (session_id, id),
|
|
22
|
+
UNIQUE (session_id, seq)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_entries_session_seq ON entries(session_id, seq);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_entries_session_parent ON entries(session_id, parent_id);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_entries_session_type_seq ON entries(session_id, type, seq);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS session_sequences (
|
|
30
|
+
session_id TEXT PRIMARY KEY,
|
|
31
|
+
next_seq INTEGER NOT NULL
|
|
32
|
+
) WITHOUT ROWID;
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS session_stats (
|
|
35
|
+
session_id TEXT PRIMARY KEY,
|
|
36
|
+
message_count INTEGER NOT NULL,
|
|
37
|
+
cached_tokens REAL NOT NULL,
|
|
38
|
+
uncached_tokens REAL NOT NULL,
|
|
39
|
+
total_tokens REAL NOT NULL,
|
|
40
|
+
cost_total REAL NOT NULL
|
|
41
|
+
) WITHOUT ROWID;
|
|
42
|
+
|
|
43
|
+
-- Derived branch cache. Parent links in entries remain canonical; this cache
|
|
44
|
+
-- exists only to make branch scans cheap.
|
|
45
|
+
CREATE TABLE IF NOT EXISTS branch_entries (
|
|
46
|
+
session_id TEXT NOT NULL,
|
|
47
|
+
branch_id TEXT NOT NULL,
|
|
48
|
+
entry_id TEXT NOT NULL,
|
|
49
|
+
entry_seq INTEGER NOT NULL,
|
|
50
|
+
entry_type TEXT NULL,
|
|
51
|
+
custom_type TEXT NULL,
|
|
52
|
+
PRIMARY KEY (session_id, branch_id, entry_id)
|
|
53
|
+
) WITHOUT ROWID;
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_branch_entries_session_branch_seq ON branch_entries(session_id, branch_id, entry_seq);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_branch_entries_session_entry ON branch_entries(session_id, entry_id);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_branch_entries_session_branch_type_seq ON branch_entries(session_id, branch_id, entry_type, entry_seq);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_branch_entries_session_branch_custom_seq ON branch_entries(session_id, branch_id, custom_type, entry_seq);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS lanes (
|
|
61
|
+
session_id TEXT NOT NULL,
|
|
62
|
+
lane TEXT NOT NULL,
|
|
63
|
+
leaf_id TEXT NULL,
|
|
64
|
+
open_operation_id TEXT NULL,
|
|
65
|
+
PRIMARY KEY (session_id, lane)
|
|
66
|
+
) WITHOUT ROWID;
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
69
|
+
session_id TEXT NOT NULL,
|
|
70
|
+
seq INTEGER NOT NULL,
|
|
71
|
+
id TEXT NOT NULL,
|
|
72
|
+
lane TEXT NOT NULL,
|
|
73
|
+
run_id TEXT NULL,
|
|
74
|
+
type TEXT NOT NULL,
|
|
75
|
+
op_kind TEXT NULL,
|
|
76
|
+
timestamp TEXT NOT NULL,
|
|
77
|
+
payload TEXT NOT NULL,
|
|
78
|
+
PRIMARY KEY (session_id, id),
|
|
79
|
+
UNIQUE (session_id, seq)
|
|
80
|
+
) WITHOUT ROWID;
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_records_session_seq ON records(session_id, seq);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_records_session_lane_type_seq ON records(session_id, lane, type, seq);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_records_session_lane_type_op_kind_seq ON records(session_id, lane, type, op_kind, seq);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_records_session_run_id_seq ON records(session_id, run_id, seq);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS lane_moves (
|
|
88
|
+
session_id TEXT NOT NULL,
|
|
89
|
+
seq INTEGER NOT NULL,
|
|
90
|
+
lane TEXT NOT NULL,
|
|
91
|
+
leaf_id TEXT NULL,
|
|
92
|
+
PRIMARY KEY (session_id, seq)
|
|
93
|
+
) WITHOUT ROWID;
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_lane_moves_session_lane_seq ON lane_moves(session_id, lane, seq);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
98
|
+
session_id TEXT NOT NULL,
|
|
99
|
+
seq INTEGER NOT NULL,
|
|
100
|
+
kind TEXT NOT NULL,
|
|
101
|
+
key TEXT NULL,
|
|
102
|
+
value TEXT NULL,
|
|
103
|
+
PRIMARY KEY (session_id, seq)
|
|
104
|
+
) WITHOUT ROWID;
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_facts_session_kind_key_seq ON facts(session_id, kind, key, seq);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS branch_tips (
|
|
109
|
+
session_id TEXT NOT NULL,
|
|
110
|
+
tip_id TEXT NOT NULL,
|
|
111
|
+
branch_id TEXT NOT NULL,
|
|
112
|
+
PRIMARY KEY (session_id, tip_id),
|
|
113
|
+
UNIQUE (session_id, branch_id)
|
|
114
|
+
) WITHOUT ROWID;
|
|
115
|
+
|
|
116
|
+
-- Per-session writer claim. The fence prevents an expired owner from writing
|
|
117
|
+
-- after a new owner takes over the session.
|
|
118
|
+
CREATE TABLE IF NOT EXISTS writer_leases (
|
|
119
|
+
session_id TEXT PRIMARY KEY,
|
|
120
|
+
owner_id TEXT NOT NULL,
|
|
121
|
+
fence INTEGER NOT NULL,
|
|
122
|
+
expires_at_ms INTEGER NOT NULL
|
|
123
|
+
) WITHOUT ROWID;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SqliteDatabase } from "./types.ts";
|
|
2
|
+
export interface SqliteMigration {
|
|
3
|
+
id: string;
|
|
4
|
+
order: number;
|
|
5
|
+
sql: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadMigrations(): Promise<SqliteMigration[]>;
|
|
8
|
+
export declare function applyMigrations(db: SqliteDatabase): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAMD,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAQjE;AAWD,wBAAsB,eAAe,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBvE","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport type { SqliteDatabase } from \"./types.ts\";\n\nexport interface SqliteMigration {\n\tid: string;\n\torder: number;\n\tsql: string;\n}\n\nasync function loadMigrationSql(relativePath: string): Promise<string> {\n\treturn readFile(fileURLToPath(new URL(relativePath, import.meta.url)), \"utf8\");\n}\n\nexport async function loadMigrations(): Promise<SqliteMigration[]> {\n\treturn [\n\t\t{\n\t\t\tid: \"001_initial.sql\",\n\t\t\torder: 1,\n\t\t\tsql: await loadMigrationSql(\"./migrations/001_initial.sql\"),\n\t\t},\n\t];\n}\n\nfunction ensureMigrationsTable(db: SqliteDatabase): void {\n\tdb.exec(`\nCREATE TABLE IF NOT EXISTS migrations (\n\tid TEXT PRIMARY KEY,\n\tapplied_at TEXT NOT NULL\n);\n`);\n}\n\nexport async function applyMigrations(db: SqliteDatabase): Promise<void> {\n\tensureMigrationsTable(db);\n\tconst migrations = await loadMigrations();\n\tconst appliedRows = db.prepare(\"SELECT id FROM migrations ORDER BY applied_at, id\").all<{ id: string }>();\n\tconst applied = new Set(appliedRows.map((row) => row.id));\n\n\tfor (const migration of migrations) {\n\t\tif (applied.has(migration.id)) continue;\n\t\tdb.transaction(() => {\n\t\t\tdb.exec(migration.sql);\n\t\t\tdb.prepare(\"INSERT INTO migrations (id, applied_at) VALUES (?, ?)\").run(\n\t\t\t\tmigration.id,\n\t\t\t\tnew Date().toISOString(),\n\t\t\t);\n\t\t});\n\t\tapplied.add(migration.id);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
async function loadMigrationSql(relativePath) {
|
|
4
|
+
return readFile(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
|
|
5
|
+
}
|
|
6
|
+
export async function loadMigrations() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
id: "001_initial.sql",
|
|
10
|
+
order: 1,
|
|
11
|
+
sql: await loadMigrationSql("./migrations/001_initial.sql"),
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
function ensureMigrationsTable(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
applied_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
export async function applyMigrations(db) {
|
|
24
|
+
ensureMigrationsTable(db);
|
|
25
|
+
const migrations = await loadMigrations();
|
|
26
|
+
const appliedRows = db.prepare("SELECT id FROM migrations ORDER BY applied_at, id").all();
|
|
27
|
+
const applied = new Set(appliedRows.map((row) => row.id));
|
|
28
|
+
for (const migration of migrations) {
|
|
29
|
+
if (applied.has(migration.id))
|
|
30
|
+
continue;
|
|
31
|
+
db.transaction(() => {
|
|
32
|
+
db.exec(migration.sql);
|
|
33
|
+
db.prepare("INSERT INTO migrations (id, applied_at) VALUES (?, ?)").run(migration.id, new Date().toISOString());
|
|
34
|
+
});
|
|
35
|
+
applied.add(migration.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AASzC,KAAK,UAAU,gBAAgB,CAAC,YAAoB,EAAmB;IACtE,OAAO,QAAQ,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,YAAY,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAAA,CAC/E;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,GAA+B;IAClE,OAAO;QACN;YACC,EAAE,EAAE,iBAAiB;YACrB,KAAK,EAAE,CAAC;YACR,GAAG,EAAE,MAAM,gBAAgB,CAAC,8BAA8B,CAAC;SAC3D;KACD,CAAC;AAAA,CACF;AAED,SAAS,qBAAqB,CAAC,EAAkB,EAAQ;IACxD,EAAE,CAAC,IAAI,CAAC;;;;;CAKR,CAAC,CAAC;AAAA,CACF;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EAAkB,EAAiB;IACxE,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAC1B,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,mDAAmD,CAAC,CAAC,GAAG,EAAkB,CAAC;IAC1G,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAE1D,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,SAAS;QACxC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC;YACpB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACvB,EAAE,CAAC,OAAO,CAAC,uDAAuD,CAAC,CAAC,GAAG,CACtE,SAAS,CAAC,EAAE,EACZ,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CACxB,CAAC;QAAA,CACF,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;AAAA,CACD","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport type { SqliteDatabase } from \"./types.ts\";\n\nexport interface SqliteMigration {\n\tid: string;\n\torder: number;\n\tsql: string;\n}\n\nasync function loadMigrationSql(relativePath: string): Promise<string> {\n\treturn readFile(fileURLToPath(new URL(relativePath, import.meta.url)), \"utf8\");\n}\n\nexport async function loadMigrations(): Promise<SqliteMigration[]> {\n\treturn [\n\t\t{\n\t\t\tid: \"001_initial.sql\",\n\t\t\torder: 1,\n\t\t\tsql: await loadMigrationSql(\"./migrations/001_initial.sql\"),\n\t\t},\n\t];\n}\n\nfunction ensureMigrationsTable(db: SqliteDatabase): void {\n\tdb.exec(`\nCREATE TABLE IF NOT EXISTS migrations (\n\tid TEXT PRIMARY KEY,\n\tapplied_at TEXT NOT NULL\n);\n`);\n}\n\nexport async function applyMigrations(db: SqliteDatabase): Promise<void> {\n\tensureMigrationsTable(db);\n\tconst migrations = await loadMigrations();\n\tconst appliedRows = db.prepare(\"SELECT id FROM migrations ORDER BY applied_at, id\").all<{ id: string }>();\n\tconst applied = new Set(appliedRows.map((row) => row.id));\n\n\tfor (const migration of migrations) {\n\t\tif (applied.has(migration.id)) continue;\n\t\tdb.transaction(() => {\n\t\t\tdb.exec(migration.sql);\n\t\t\tdb.prepare(\"INSERT INTO migrations (id, applied_at) VALUES (?, ?)\").run(\n\t\t\t\tmigration.id,\n\t\t\t\tnew Date().toISOString(),\n\t\t\t);\n\t\t});\n\t\tapplied.add(migration.id);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ForkOptions, Session, type SessionRepo as SessionRepository } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { SqliteDatabaseFactory, SqliteSessionCreateOptions, SqliteSessionListOptions, SqliteSessionMetadata, SqliteSessionRepositoryEnv } from "./types.ts";
|
|
3
|
+
export interface SqliteWriterLeaseOptions {
|
|
4
|
+
/** Time without a successful heartbeat before another writer may take over. Default: 30 seconds. */
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
/** Idle heartbeat cadence. Default: 10 seconds. Must be less than ttlMs. */
|
|
7
|
+
heartbeatIntervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SqliteSessionRepositoryOptions {
|
|
10
|
+
env: SqliteSessionRepositoryEnv;
|
|
11
|
+
sqlite: SqliteDatabaseFactory;
|
|
12
|
+
databasePath: string;
|
|
13
|
+
writerLease?: SqliteWriterLeaseOptions;
|
|
14
|
+
}
|
|
15
|
+
export declare class SqliteSessionRepository implements SessionRepository<SqliteSessionMetadata, SqliteSessionCreateOptions, SqliteSessionListOptions>, AsyncDisposable {
|
|
16
|
+
private databasePath;
|
|
17
|
+
private database;
|
|
18
|
+
private databasePromise;
|
|
19
|
+
private readonly operations;
|
|
20
|
+
private readonly activeStorages;
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly leaseOptions;
|
|
23
|
+
constructor(options: SqliteSessionRepositoryOptions);
|
|
24
|
+
private releaseStoragesForSession;
|
|
25
|
+
private sessionFromLease;
|
|
26
|
+
private claimSession;
|
|
27
|
+
create(options: SqliteSessionCreateOptions): Promise<Session<SqliteSessionMetadata>>;
|
|
28
|
+
open(metadata: SqliteSessionMetadata): Promise<Session<SqliteSessionMetadata>>;
|
|
29
|
+
/** Rebuilds this session's private branch-read cache from canonical entry parent links. */
|
|
30
|
+
repairBranchCache(metadata: SqliteSessionMetadata): Promise<void>;
|
|
31
|
+
/** Reads the session catalog without acquiring or renewing per-session writer leases. */
|
|
32
|
+
list(options?: SqliteSessionListOptions): Promise<SqliteSessionMetadata[]>;
|
|
33
|
+
delete(metadata: SqliteSessionMetadata): Promise<void>;
|
|
34
|
+
fork(source: SqliteSessionMetadata, options: ForkOptions & SqliteSessionCreateOptions): Promise<Session<SqliteSessionMetadata>>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
37
|
+
private getDatabasePath;
|
|
38
|
+
private getDatabase;
|
|
39
|
+
private openDatabase;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/sqlite/repo.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,WAAW,EAQhB,OAAO,EAEP,KAAK,WAAW,IAAI,iBAAiB,EAGrC,MAAM,+BAA+B,CAAC;AAkEvC,OAAO,KAAK,EAEX,qBAAqB,EACrB,0BAA0B,EAC1B,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,wBAAwB;IACxC,oGAAoG;IACpG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,8BAA8B;IAC9C,GAAG,EAAE,0BAA0B,CAAC;IAChC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,wBAAwB,CAAC;CACvC;AAoiBD,qBAAa,uBACZ,YACC,iBAAiB,CAAC,qBAAqB,EAAE,0BAA0B,EAAE,wBAAwB,CAAC,EAC9F,eAAe;IAEhB,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmC;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAE1D,YAAY,OAAO,EAAE,8BAA8B,EAGlD;YAEa,yBAAyB;IAMvC,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,YAAY;IAcd,MAAM,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAuBzF;IAEK,IAAI,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAEnF;IAED,2FAA2F;IACrF,iBAAiB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWtE;IAED,yFAAyF;IACnF,IAAI,CAAC,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAQnF;IAEK,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3D;IAEK,IAAI,CACT,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,WAAW,GAAG,0BAA0B,GAC/C,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CA6GzC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAM3B;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;YAEa,eAAe;YAQf,WAAW;YAMX,YAAY;CAgB1B","sourcesContent":["import type { FileError, Result } from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype BranchBounds,\n\ttype Entry,\n\ttype EntryQuery,\n\ttype ForkOptions,\n\ttype LaneRecord,\n\ttype LogItem,\n\ttype LogOptions,\n\ttype NewRecord,\n\ttype OperationStartedRecord,\n\ttype ProvisionedEntry,\n\ttype RecordQuery,\n\tSession,\n\tSessionError,\n\ttype SessionRepo as SessionRepository,\n\ttype SessionStats,\n\ttype SessionStorage,\n} from \"@earendil-works/pi-agent-core\";\nimport { uuidv7 } from \"@earendil-works/pi-ai\";\nimport { appendEntryToBranchCache, buildCachedBranch, deleteBranchCache, rebuildBranchCache } from \"./branch-cache.ts\";\nimport { applyMigrations } from \"./migrations.ts\";\nimport { type CachedBranchEntryRow, queryCachedBranchRows, readCachedBranch } from \"./storage/branch-entries.ts\";\nimport { readBranchTipIds } from \"./storage/branch-tips.ts\";\nimport {\n\tdeleteEntryRows,\n\ttype EntryRow,\n\tentryPayload,\n\tidExistsInEntries,\n\tinsertEntryRow,\n\treadEntryRow,\n\treadEntryRows,\n} from \"./storage/entries.ts\";\nimport { appendFact, deleteFactRows, readFactRows, readLatestFact, readLatestLabelFacts } from \"./storage/facts.ts\";\nimport {\n\tcreateInitialLane,\n\tdeleteLaneRows,\n\tfinishLaneOperation,\n\tcreateLane as insertLane,\n\treadLane,\n\treadLaneHead,\n\treadLaneMoveRows,\n\treadLanes,\n\tsetLaneLeaf,\n\tstartLaneOperation,\n\tmoveLane as updateLane,\n} from \"./storage/lanes.ts\";\nimport {\n\tappendRecordRow,\n\tdeleteRecordRows,\n\tidExistsInRecords,\n\treadOpenOperationRows,\n\treadRecordRows,\n} from \"./storage/records.ts\";\nimport {\n\tadvanceSequence,\n\tcreateSequence,\n\tdeleteSequence,\n\tgetNextSequence,\n\tsetNextSequence,\n} from \"./storage/session-sequences.ts\";\nimport {\n\taddUsageToStats,\n\tcreateStats,\n\tdeleteStats,\n\tincrementMessageCount,\n\treadStats,\n} from \"./storage/session-stats.ts\";\nimport {\n\tdecodeSessionMetadata,\n\tdeleteSessionRow,\n\tinsertSessionRow,\n\treadSessionRow,\n\treadSessionRows,\n\ttype SessionRow,\n\tsessionExists,\n} from \"./storage/sessions.ts\";\nimport {\n\tacquireWriterLease,\n\tdeleteWriterLease,\n\treleaseWriterLease,\n\trenewWriterLease,\n\ttype WriterLease,\n} from \"./storage/writer-leases.ts\";\nimport type {\n\tSqliteDatabase,\n\tSqliteDatabaseFactory,\n\tSqliteSessionCreateOptions,\n\tSqliteSessionListOptions,\n\tSqliteSessionMetadata,\n\tSqliteSessionRepositoryEnv,\n} from \"./types.ts\";\n\nexport interface SqliteWriterLeaseOptions {\n\t/** Time without a successful heartbeat before another writer may take over. Default: 30 seconds. */\n\tttlMs?: number;\n\t/** Idle heartbeat cadence. Default: 10 seconds. Must be less than ttlMs. */\n\theartbeatIntervalMs?: number;\n}\n\nexport interface SqliteSessionRepositoryOptions {\n\tenv: SqliteSessionRepositoryEnv;\n\tsqlite: SqliteDatabaseFactory;\n\tdatabasePath: string;\n\twriterLease?: SqliteWriterLeaseOptions;\n}\n\ninterface ResolvedWriterLeaseOptions {\n\tttlMs: number;\n\theartbeatIntervalMs: number;\n}\n\nfunction resolveWriterLeaseOptions(options: SqliteWriterLeaseOptions | undefined): ResolvedWriterLeaseOptions {\n\tconst ttlMs = options?.ttlMs ?? 30_000;\n\tconst heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 10_000;\n\tif (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"writerLease.ttlMs must be positive\");\n\tif (!Number.isSafeInteger(heartbeatIntervalMs) || heartbeatIntervalMs <= 0 || heartbeatIntervalMs >= ttlMs) {\n\t\tthrow new RangeError(\"writerLease.heartbeatIntervalMs must be positive and less than ttlMs\");\n\t}\n\treturn { ttlMs, heartbeatIntervalMs };\n}\n\nfunction activeWriterError(sessionId: string): SessionError {\n\treturn new SessionError(\"storage\", `SQLite session ${sessionId} already has an active writer`);\n}\n\nfunction lostWriterError(sessionId: string): SessionError {\n\treturn new SessionError(\"storage\", `SQLite session ${sessionId} writer lease was lost`);\n}\n\nfunction claimWriterLease(db: SqliteDatabase, sessionId: string, options: ResolvedWriterLeaseOptions): WriterLease {\n\tconst now = Date.now();\n\tconst lease = acquireWriterLease(db, sessionId, uuidv7(), now, now + options.ttlMs);\n\tif (!lease) throw activeWriterError(sessionId);\n\treturn lease;\n}\n\nclass SerialOperationQueue {\n\tprivate tail: Promise<void> = Promise.resolve();\n\n\tenqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n\t\tconst result = this.tail.then(operation);\n\t\tthis.tail = result.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\treturn result;\n\t}\n\n\tasync drain(): Promise<void> {\n\t\tawait this.tail;\n\t}\n}\n\nfunction resultOrThrow<T>(result: Result<T, FileError>, message: string): T {\n\tif (!result.ok) {\n\t\tconst code = result.error.code === \"not_found\" ? \"not_found\" : \"storage\";\n\t\tthrow new SessionError(code, `${message}: ${result.error.message}`, result.error);\n\t}\n\treturn result.value;\n}\n\nfunction getParentPath(path: string): string {\n\tconst normalized = path.replace(/[\\\\/]+$/, \"\");\n\tconst lastSlash = Math.max(normalized.lastIndexOf(\"/\"), normalized.lastIndexOf(\"\\\\\"));\n\tif (lastSlash < 0) return \".\";\n\tif (lastSlash === 0) return normalized.slice(0, 1);\n\treturn normalized.slice(0, lastSlash);\n}\n\nfunction configureSqliteDatabase(db: SqliteDatabase): void {\n\tdb.exec(\"PRAGMA journal_mode=WAL\");\n\tdb.exec(\"PRAGMA synchronous=FULL\");\n\tdb.exec(\"PRAGMA busy_timeout=5000\");\n}\n\nfunction timestampToText(timestamp: number): string {\n\treturn new Date(timestamp).toISOString();\n}\n\nfunction timestampFromText(timestamp: string): number {\n\treturn Date.parse(timestamp);\n}\n\nfunction entryRowFromCached(row: CachedBranchEntryRow): EntryRow {\n\treturn { ...row, seq: row.entry_seq, type: row.type as Entry[\"type\"] };\n}\n\nfunction readObjectPayload(row: EntryRow): Record<string, unknown> {\n\tconst payload = JSON.parse(row.payload) as unknown;\n\tif (typeof payload !== \"object\" || payload === null || Array.isArray(payload)) {\n\t\tthrow new Error(\"Payload is not an object\");\n\t}\n\treturn payload as Record<string, unknown>;\n}\n\nfunction decodeEntry(row: EntryRow): Entry {\n\ttry {\n\t\tconst payload = readObjectPayload(row);\n\t\tconst timestamp = timestampFromText(row.timestamp);\n\t\tif (!Number.isFinite(timestamp)) throw new Error(`Invalid timestamp ${row.timestamp}`);\n\t\tconst base = { id: row.id, seq: row.seq, parentId: row.parent_id, timestamp };\n\t\tswitch (row.type) {\n\t\t\tcase \"message\":\n\t\t\t\tif (typeof payload.message !== \"object\" || payload.message === null) throw new Error(\"Missing message\");\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\tmessage: payload.message as Extract<Entry, { type: \"message\" }>[\"message\"],\n\t\t\t\t\t...(payload.terminate === true ? { terminate: true as const } : {}),\n\t\t\t\t};\n\t\t\tcase \"model_change\":\n\t\t\t\tif (typeof payload.provider !== \"string\" || typeof payload.modelId !== \"string\") {\n\t\t\t\t\tthrow new Error(\"Invalid model_change payload\");\n\t\t\t\t}\n\t\t\t\treturn { ...base, type: \"model_change\", provider: payload.provider, modelId: payload.modelId };\n\t\t\tcase \"thinking_level_change\":\n\t\t\t\tif (typeof payload.thinkingLevel !== \"string\") throw new Error(\"Invalid thinking_level_change payload\");\n\t\t\t\treturn { ...base, type: \"thinking_level_change\", thinkingLevel: payload.thinkingLevel };\n\t\t\tcase \"active_tools_change\":\n\t\t\t\tif (!Array.isArray(payload.activeToolNames)) throw new Error(\"Invalid active_tools_change payload\");\n\t\t\t\tif (payload.activeToolNames.some((value) => typeof value !== \"string\")) {\n\t\t\t\t\tthrow new Error(\"Invalid active_tools_change payload\");\n\t\t\t\t}\n\t\t\t\treturn { ...base, type: \"active_tools_change\", activeToolNames: payload.activeToolNames };\n\t\t\tcase \"compaction\":\n\t\t\t\tif (\n\t\t\t\t\ttypeof payload.summary !== \"string\" ||\n\t\t\t\t\t!Array.isArray(payload.retainedTail) ||\n\t\t\t\t\ttypeof payload.tokensBefore !== \"number\"\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\"Invalid compaction payload\");\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\ttype: \"compaction\",\n\t\t\t\t\tsummary: payload.summary,\n\t\t\t\t\tretainedTail: payload.retainedTail as Extract<Entry, { type: \"compaction\" }>[\"retainedTail\"],\n\t\t\t\t\ttokensBefore: payload.tokensBefore,\n\t\t\t\t\t...(Object.hasOwn(payload, \"details\") ? { details: payload.details } : {}),\n\t\t\t\t\t...(Object.hasOwn(payload, \"usage\")\n\t\t\t\t\t\t? { usage: payload.usage as Extract<Entry, { type: \"compaction\" }>[\"usage\"] }\n\t\t\t\t\t\t: {}),\n\t\t\t\t};\n\t\t\tcase \"branch_summary\":\n\t\t\t\tif (typeof payload.fromId !== \"string\" || typeof payload.summary !== \"string\") {\n\t\t\t\t\tthrow new Error(\"Invalid branch_summary payload\");\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\ttype: \"branch_summary\",\n\t\t\t\t\tfromId: payload.fromId,\n\t\t\t\t\tsummary: payload.summary,\n\t\t\t\t\t...(Object.hasOwn(payload, \"details\") ? { details: payload.details } : {}),\n\t\t\t\t\t...(Object.hasOwn(payload, \"usage\")\n\t\t\t\t\t\t? { usage: payload.usage as Extract<Entry, { type: \"branch_summary\" }>[\"usage\"] }\n\t\t\t\t\t\t: {}),\n\t\t\t\t};\n\t\t\tcase \"custom\":\n\t\t\t\tif (typeof payload.customType !== \"string\") throw new Error(\"Invalid custom payload\");\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\ttype: \"custom\",\n\t\t\t\t\tcustomType: payload.customType,\n\t\t\t\t\t...(Object.hasOwn(payload, \"data\") ? { data: payload.data } : {}),\n\t\t\t\t};\n\t\t}\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"invalid_entry\",\n\t\t\t`Invalid SQLite session entry ${row.id}: failed to decode entry ${row.id}`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n}\n\nfunction recordRunId(record: NewRecord): string | undefined {\n\treturn record.type === \"operation_started\" ? record.id : \"runId\" in record ? record.runId : undefined;\n}\n\nfunction recordOpKind(record: NewRecord): string | undefined {\n\treturn record.type === \"operation_started\" ? record.intent.kind : undefined;\n}\n\nfunction decodeRecord(row: { seq: number; timestamp: string; payload: string }): LaneRecord {\n\ttry {\n\t\tconst timestamp = timestampFromText(row.timestamp);\n\t\tif (!Number.isFinite(timestamp)) throw new Error(`Invalid timestamp ${row.timestamp}`);\n\t\treturn {\n\t\t\t...(JSON.parse(row.payload) as object),\n\t\t\tseq: row.seq,\n\t\t\ttimestamp,\n\t\t} as LaneRecord;\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Invalid SQLite session record at sequence ${row.seq}: failed to decode payload`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n}\n\nfunction validateCachedBranchRows(rows: readonly CachedBranchEntryRow[], query: BranchBounds): void {\n\tif (rows.length === 0) return;\n\tconst path = [...rows].sort((left, right) => left.entry_seq - right.entry_seq);\n\tif (query.stopAtId === undefined && query.stopAtType === undefined && path[0]?.parent_id !== null) {\n\t\tthrow new SessionError(\"invalid_entry\", `Entry ${path[0]?.parent_id} not found`);\n\t}\n\tfor (let index = 1; index < path.length; index++) {\n\t\tconst previous = path[index - 1]!;\n\t\tconst current = path[index]!;\n\t\tif (current.parent_id !== previous.id) {\n\t\t\tthrow new SessionError(\"invalid_entry\", `Entry ${current.parent_id} not found`);\n\t\t}\n\t}\n}\n\nfunction matchesEntryQuery(entry: Entry, query: EntryQuery): boolean {\n\treturn (\n\t\t(query.type === undefined || entry.type === query.type) &&\n\t\t(query.customType === undefined || (entry.type === \"custom\" && entry.customType === query.customType)) &&\n\t\t(query.cursor === undefined ||\n\t\t\t(query.order === \"oldestFirst\" ? entry.seq > query.cursor.afterSeq : entry.seq < query.cursor.afterSeq))\n\t);\n}\n\nfunction assertUnusedId(db: SqliteDatabase, sessionId: string, id: string): void {\n\tif (idExistsInEntries(db, sessionId, id) || idExistsInRecords(db, sessionId, id)) {\n\t\tthrow new SessionError(\"already_exists\", `ID already exists: ${id}`);\n\t}\n}\n\nfunction requireSessionRow(db: SqliteDatabase, sessionId: string): SessionRow {\n\tconst row = readSessionRow(db, sessionId);\n\tif (!row) throw new SessionError(\"not_found\", `Session not found: ${sessionId}`);\n\treturn row;\n}\n\nclass SqliteSessionStorage implements SessionStorage<SqliteSessionMetadata> {\n\tprivate readonly db: SqliteDatabase;\n\tprivate readonly metadata: SqliteSessionMetadata;\n\tprivate readonly lease: WriterLease;\n\tprivate readonly leaseOptions: ResolvedWriterLeaseOptions;\n\tprivate readonly onRelease: () => void;\n\tprivate readonly operations = new SerialOperationQueue();\n\tprivate heartbeatTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate leaseError: SessionError | undefined;\n\tprivate closing = false;\n\tprivate releasePromise: Promise<void> | undefined;\n\n\tconstructor(\n\t\tdb: SqliteDatabase,\n\t\tmetadata: SqliteSessionMetadata,\n\t\tlease: WriterLease,\n\t\tleaseOptions: ResolvedWriterLeaseOptions,\n\t\tonRelease: () => void,\n\t) {\n\t\tthis.db = db;\n\t\tthis.metadata = metadata;\n\t\tthis.lease = lease;\n\t\tthis.leaseOptions = leaseOptions;\n\t\tthis.onRelease = onRelease;\n\t\tthis.scheduleHeartbeat();\n\t}\n\n\tasync release(): Promise<void> {\n\t\tthis.releasePromise ??= this.finishRelease();\n\t\tawait this.releasePromise;\n\t}\n\n\tprivate async finishRelease(): Promise<void> {\n\t\tthis.closing = true;\n\t\tif (this.heartbeatTimer !== undefined) clearTimeout(this.heartbeatTimer);\n\t\ttry {\n\t\t\tawait this.operations.enqueue(() =>\n\t\t\t\tthis.db.transaction(() => releaseWriterLease(this.db, this.metadata.id, this.lease)),\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.onRelease();\n\t\t}\n\t}\n\n\tprivate enqueueWrite<T>(operation: () => T): Promise<T> {\n\t\tif (this.closing)\n\t\t\treturn Promise.reject(new SessionError(\"storage\", `SQLite session ${this.metadata.id} is closed`));\n\t\treturn this.operations.enqueue(() => {\n\t\t\tif (this.leaseError) throw this.leaseError;\n\t\t\treturn this.db.transaction(() => {\n\t\t\t\tconst now = Date.now();\n\t\t\t\tif (!renewWriterLease(this.db, this.metadata.id, this.lease, now, now + this.leaseOptions.ttlMs)) {\n\t\t\t\t\tthis.leaseError = lostWriterError(this.metadata.id);\n\t\t\t\t\tif (this.heartbeatTimer !== undefined) clearTimeout(this.heartbeatTimer);\n\t\t\t\t\tthrow this.leaseError;\n\t\t\t\t}\n\t\t\t\treturn operation();\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate scheduleHeartbeat(): void {\n\t\tif (this.closing || this.leaseError) return;\n\t\tthis.heartbeatTimer = setTimeout(async () => {\n\t\t\tthis.heartbeatTimer = undefined;\n\t\t\ttry {\n\t\t\t\tawait this.operations.enqueue(() => {\n\t\t\t\t\tif (this.closing || this.leaseError) return;\n\t\t\t\t\tthis.db.transaction(() => {\n\t\t\t\t\t\tconst now = Date.now();\n\t\t\t\t\t\tif (!renewWriterLease(this.db, this.metadata.id, this.lease, now, now + this.leaseOptions.ttlMs)) {\n\t\t\t\t\t\t\tthis.leaseError = lostWriterError(this.metadata.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// A transient heartbeat failure is retried. Every write still verifies ownership transactionally.\n\t\t\t} finally {\n\t\t\t\tthis.scheduleHeartbeat();\n\t\t\t}\n\t\t}, this.leaseOptions.heartbeatIntervalMs);\n\t\tthis.heartbeatTimer.unref();\n\t}\n\n\tasync getMetadata(): Promise<SqliteSessionMetadata> {\n\t\treturn decodeSessionMetadata(requireSessionRow(this.db, this.metadata.id), this.metadata.path);\n\t}\n\n\tisForSession(sessionId: string): boolean {\n\t\treturn this.metadata.id === sessionId;\n\t}\n\n\tasync getLanes(): Promise<{ lane: string; leafId: string | null }[]> {\n\t\treturn readLanes(this.db, this.metadata.id).map((row) => ({ lane: row.lane, leafId: row.leaf_id }));\n\t}\n\n\tasync createLane(lane: string, at: string | null): Promise<void> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tif (readLane(this.db, this.metadata.id, lane)) {\n\t\t\t\tthrow new SessionError(\"already_exists\", `Lane already exists: ${lane}`);\n\t\t\t}\n\t\t\tif (at !== null && !readEntryRow(this.db, this.metadata.id, at)) {\n\t\t\t\tthrow new SessionError(\"not_found\", `Entry not found: ${at}`);\n\t\t\t}\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tinsertLane(this.db, this.metadata.id, seq, lane, at);\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t});\n\t}\n\n\tasync moveLane(lane: string, to: string | null): Promise<void> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tif (!readLane(this.db, this.metadata.id, lane))\n\t\t\t\tthrow new SessionError(\"invalid_lane\", `Lane not found: ${lane}`);\n\t\t\tif (to !== null && !readEntryRow(this.db, this.metadata.id, to)) {\n\t\t\t\tthrow new SessionError(\"not_found\", `Entry not found: ${to}`);\n\t\t\t}\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tupdateLane(this.db, this.metadata.id, seq, lane, to);\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t});\n\t}\n\n\tasync appendEntry<TEntry extends Entry>(entry: ProvisionedEntry<TEntry>, lane: string): Promise<TEntry> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tconst parentId = readLaneHead(this.db, this.metadata.id, lane).leafId;\n\t\t\tassertUnusedId(this.db, this.metadata.id, entry.id);\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tconst committed = { ...entry, parentId, seq, timestamp: Date.now() } as Entry;\n\t\t\tinsertEntryRow(this.db, this.metadata.id, {\n\t\t\t\tseq,\n\t\t\t\tid: committed.id,\n\t\t\t\tparentId: committed.parentId,\n\t\t\t\ttype: committed.type,\n\t\t\t\ttimestamp: timestampToText(committed.timestamp),\n\t\t\t\tpayload: JSON.stringify(entryPayload(committed)),\n\t\t\t});\n\t\t\tsetLaneLeaf(this.db, this.metadata.id, lane, committed.id);\n\t\t\tappendEntryToBranchCache(\n\t\t\t\tthis.db,\n\t\t\t\tthis.metadata.id,\n\t\t\t\tcommitted.id,\n\t\t\t\tseq,\n\t\t\t\tcommitted.type,\n\t\t\t\tcommitted.type === \"custom\" ? committed.customType : null,\n\t\t\t\tcommitted.parentId,\n\t\t\t);\n\t\t\tif (committed.type === \"message\") incrementMessageCount(this.db, this.metadata.id);\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t\treturn structuredClone(committed as TEntry);\n\t\t});\n\t}\n\n\tasync appendRecord<TRecord extends LaneRecord>(record: NewRecord<TRecord>): Promise<TRecord>;\n\tasync appendRecord(record: NewRecord): Promise<LaneRecord> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tif (!readLane(this.db, this.metadata.id, record.lane)) {\n\t\t\t\tthrow new SessionError(\"invalid_lane\", `Lane not found: ${record.lane}`);\n\t\t\t}\n\t\t\tassertUnusedId(this.db, this.metadata.id, record.id);\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tconst committed: LaneRecord = { ...record, seq, timestamp: Date.now() };\n\t\t\tif (record.type === \"operation_started\") {\n\t\t\t\tstartLaneOperation(this.db, this.metadata.id, record.lane, record.id);\n\t\t\t}\n\t\t\tappendRecordRow(this.db, this.metadata.id, {\n\t\t\t\tseq,\n\t\t\t\tid: record.id,\n\t\t\t\tlane: record.lane,\n\t\t\t\trunId: recordRunId(record),\n\t\t\t\ttype: record.type,\n\t\t\t\topKind: recordOpKind(record),\n\t\t\t\ttimestamp: timestampToText(committed.timestamp),\n\t\t\t\tpayload: JSON.stringify(record),\n\t\t\t});\n\t\t\tif (record.type === \"operation_finished\") {\n\t\t\t\tfinishLaneOperation(this.db, this.metadata.id, record.lane, record.runId);\n\t\t\t}\n\t\t\tif (record.type === \"usage\") addUsageToStats(this.db, this.metadata.id, record.usage);\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t\treturn structuredClone(committed);\n\t\t});\n\t}\n\n\tasync getEntry(id: string): Promise<Entry | undefined> {\n\t\tconst row = readEntryRow(this.db, this.metadata.id, id);\n\t\treturn row ? decodeEntry(row) : undefined;\n\t}\n\n\tasync findEntries(query: EntryQuery = {}): Promise<Entry[]> {\n\t\tconst rows = readEntryRows(this.db, this.metadata.id, { order: query.order });\n\t\tconst entries = rows.map(decodeEntry).filter((entry) => matchesEntryQuery(entry, query));\n\t\treturn query.limit === undefined ? entries : entries.slice(0, query.limit);\n\t}\n\n\tasync findEntriesOnBranch(query: EntryQuery & BranchBounds & { start: string }): Promise<Entry[]> {\n\t\tconst cached = readCachedBranch(this.db, this.metadata.id, query.start);\n\t\tif (!cached) {\n\t\t\tif (!readEntryRow(this.db, this.metadata.id, query.start))\n\t\t\t\tthrow new SessionError(\"not_found\", `Entry not found: ${query.start}`);\n\t\t\tthrow new SessionError(\"invalid_entry\", `Branch cache missing entry ${query.start}`);\n\t\t}\n\t\tconst rows = queryCachedBranchRows(this.db, this.metadata.id, cached, query);\n\t\tvalidateCachedBranchRows(rows, query);\n\t\tconst entries = rows\n\t\t\t.map(entryRowFromCached)\n\t\t\t.map(decodeEntry)\n\t\t\t.filter((entry) => matchesEntryQuery(entry, query));\n\t\treturn query.limit === undefined ? entries : entries.slice(0, query.limit);\n\t}\n\n\tasync findRecords(query: RecordQuery = {}): Promise<LaneRecord[]> {\n\t\tconst rows = readRecordRows(this.db, this.metadata.id, query);\n\t\treturn rows.map(decodeRecord);\n\t}\n\n\tasync findOpenOperations(lane: string, options?: { limit?: number }): Promise<OperationStartedRecord[]> {\n\t\tconst rows = readOpenOperationRows(this.db, this.metadata.id, lane, options);\n\n\t\treturn rows.map((row) => {\n\t\t\tconst record = decodeRecord(row);\n\t\t\tif (record.type !== \"operation_started\") {\n\t\t\t\tthrow new SessionError(\"storage\", \"Expected operation_started record\");\n\t\t\t}\n\t\t\treturn record;\n\t\t});\n\t}\n\n\tasync getLog(options: LogOptions = {}): Promise<LogItem[]> {\n\t\tconst afterSeq = options.afterSeq ?? 0;\n\t\tconst entryRows = readEntryRows(this.db, this.metadata.id, { afterSeq, order: \"oldestFirst\" });\n\t\tconst recordRows = readRecordRows(this.db, this.metadata.id, { afterSeq });\n\t\tconst laneRows = readLaneMoveRows(this.db, this.metadata.id, { afterSeq });\n\t\tconst factRows = readFactRows(this.db, this.metadata.id, { afterSeq });\n\n\t\tconst log: LogItem[] = [\n\t\t\t...entryRows.map((row) => ({ kind: \"entry\" as const, seq: row.seq, entry: decodeEntry(row) })),\n\t\t\t...recordRows.map((row) => ({ kind: \"record\" as const, seq: row.seq, record: decodeRecord(row) })),\n\t\t\t...laneRows.map((row) => ({ kind: \"lane\" as const, seq: row.seq, lane: row.lane, leafId: row.leaf_id })),\n\t\t\t...factRows.map((row) => {\n\t\t\t\tif (row.kind === \"name\")\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkind: \"fact\" as const,\n\t\t\t\t\t\tseq: row.seq,\n\t\t\t\t\t\tfact: \"name\" as const,\n\t\t\t\t\t\tname: JSON.parse(row.value ?? \"null\") as string,\n\t\t\t\t\t};\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"fact\" as const,\n\t\t\t\t\tseq: row.seq,\n\t\t\t\t\tfact: \"label\" as const,\n\t\t\t\t\ttargetId: row.key ?? \"\",\n\t\t\t\t\tlabel: row.value === null ? undefined : (JSON.parse(row.value) as string),\n\t\t\t\t};\n\t\t\t}),\n\t\t].sort((left, right) => left.seq - right.seq);\n\t\treturn options.limit === undefined ? log : log.slice(0, options.limit);\n\t}\n\n\tasync getName(): Promise<string | undefined> {\n\t\tconst row = readLatestFact(this.db, this.metadata.id, \"name\", null);\n\t\treturn row?.value === undefined || row.value === null ? undefined : (JSON.parse(row.value) as string);\n\t}\n\n\tasync setName(name: string): Promise<void> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tappendFact(this.db, this.metadata.id, seq, \"name\", null, JSON.stringify(name));\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t});\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\tconst row = readLatestFact(this.db, this.metadata.id, \"label\", id);\n\t\treturn row?.value === undefined || row.value === null ? undefined : (JSON.parse(row.value) as string);\n\t}\n\n\tasync setLabel(id: string, label: string | undefined): Promise<void> {\n\t\treturn this.enqueueWrite(() => {\n\t\t\tif (!readEntryRow(this.db, this.metadata.id, id)) {\n\t\t\t\tthrow new SessionError(\"not_found\", `Entry not found: ${id}`);\n\t\t\t}\n\t\t\tconst seq = getNextSequence(this.db, this.metadata.id);\n\t\t\tappendFact(this.db, this.metadata.id, seq, \"label\", id, label === undefined ? null : JSON.stringify(label));\n\t\t\tadvanceSequence(this.db, this.metadata.id, seq);\n\t\t});\n\t}\n\n\tasync getStats(): Promise<SessionStats> {\n\t\treturn readStats(this.db, this.metadata.id);\n\t}\n}\n\nfunction claimStorage(\n\tdb: SqliteDatabase,\n\tmetadata: SqliteSessionMetadata,\n\tleaseOptions: ResolvedWriterLeaseOptions,\n\tonRelease: () => void,\n): SqliteSessionStorage {\n\trequireSessionRow(db, metadata.id);\n\tconst claimed = db.transaction(() => {\n\t\tconst lease = claimWriterLease(db, metadata.id, leaseOptions);\n\t\tconst row = requireSessionRow(db, metadata.id);\n\t\treadLanes(db, metadata.id);\n\t\treturn { lease, row };\n\t});\n\treturn new SqliteSessionStorage(\n\t\tdb,\n\t\tdecodeSessionMetadata(claimed.row, metadata.path),\n\t\tclaimed.lease,\n\t\tleaseOptions,\n\t\tonRelease,\n\t);\n}\n\nexport class SqliteSessionRepository\n\timplements\n\t\tSessionRepository<SqliteSessionMetadata, SqliteSessionCreateOptions, SqliteSessionListOptions>,\n\t\tAsyncDisposable\n{\n\tprivate databasePath: string | undefined;\n\tprivate database: SqliteDatabase | undefined;\n\tprivate databasePromise: Promise<SqliteDatabase> | undefined;\n\tprivate readonly operations = new SerialOperationQueue();\n\tprivate readonly activeStorages = new Set<SqliteSessionStorage>();\n\tprivate readonly options: SqliteSessionRepositoryOptions;\n\tprivate readonly leaseOptions: ResolvedWriterLeaseOptions;\n\n\tconstructor(options: SqliteSessionRepositoryOptions) {\n\t\tthis.options = options;\n\t\tthis.leaseOptions = resolveWriterLeaseOptions(options.writerLease);\n\t}\n\n\tprivate async releaseStoragesForSession(sessionId: string): Promise<void> {\n\t\tfor (const storage of [...this.activeStorages]) {\n\t\t\tif (storage.isForSession(sessionId)) await storage.release();\n\t\t}\n\t}\n\n\tprivate sessionFromLease(\n\t\tdb: SqliteDatabase,\n\t\tmetadata: SqliteSessionMetadata,\n\t\tlease: WriterLease,\n\t): Session<SqliteSessionMetadata> {\n\t\tlet storage: SqliteSessionStorage;\n\t\tstorage = new SqliteSessionStorage(db, metadata, lease, this.leaseOptions, () => {\n\t\t\tthis.activeStorages.delete(storage);\n\t\t});\n\t\tthis.activeStorages.add(storage);\n\t\treturn new Session(storage);\n\t}\n\n\tprivate claimSession(db: SqliteDatabase, metadata: SqliteSessionMetadata): Session<SqliteSessionMetadata> {\n\t\tconst active = [...this.activeStorages].find((storage) => storage.isForSession(metadata.id));\n\t\tif (active) {\n\t\t\treadLanes(db, metadata.id);\n\t\t\treturn new Session(active);\n\t\t}\n\t\tlet storage: SqliteSessionStorage;\n\t\tstorage = claimStorage(db, metadata, this.leaseOptions, () => {\n\t\t\tthis.activeStorages.delete(storage);\n\t\t});\n\t\tthis.activeStorages.add(storage);\n\t\treturn new Session(storage);\n\t}\n\n\tasync create(options: SqliteSessionCreateOptions): Promise<Session<SqliteSessionMetadata>> {\n\t\treturn this.operations.enqueue(async () => {\n\t\t\tconst db = await this.getDatabase();\n\t\t\tconst path = await this.getDatabasePath();\n\t\t\tconst id = options.id ?? uuidv7();\n\t\t\tif (sessionExists(db, id)) throw new SessionError(\"already_exists\", `Session already exists: ${id}`);\n\t\t\tconst createdAt = Date.now();\n\t\t\tconst lease = db.transaction(() => {\n\t\t\t\tinsertSessionRow(db, {\n\t\t\t\t\tid,\n\t\t\t\t\tcreatedAt: timestampToText(createdAt),\n\t\t\t\t\tcwd: options.cwd,\n\t\t\t\t\tparentSessionId: options.parentSessionId,\n\t\t\t\t\tmetadata: options.metadata,\n\t\t\t\t});\n\t\t\t\tcreateSequence(db, id);\n\t\t\t\tcreateStats(db, id);\n\t\t\t\tcreateInitialLane(db, id);\n\t\t\t\treturn claimWriterLease(db, id, this.leaseOptions);\n\t\t\t});\n\t\t\tconst row = requireSessionRow(db, id);\n\t\t\treturn this.sessionFromLease(db, decodeSessionMetadata(row, path), lease);\n\t\t});\n\t}\n\n\tasync open(metadata: SqliteSessionMetadata): Promise<Session<SqliteSessionMetadata>> {\n\t\treturn this.operations.enqueue(async () => this.claimSession(await this.getDatabase(), metadata));\n\t}\n\n\t/** Rebuilds this session's private branch-read cache from canonical entry parent links. */\n\tasync repairBranchCache(metadata: SqliteSessionMetadata): Promise<void> {\n\t\treturn this.operations.enqueue(async () => {\n\t\t\tawait this.releaseStoragesForSession(metadata.id);\n\t\t\tconst db = await this.getDatabase();\n\t\t\tdb.transaction(() => {\n\t\t\t\tconst lease = claimWriterLease(db, metadata.id, this.leaseOptions);\n\t\t\t\trequireSessionRow(db, metadata.id);\n\t\t\t\trebuildBranchCache(db, metadata.id);\n\t\t\t\treleaseWriterLease(db, metadata.id, lease);\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Reads the session catalog without acquiring or renewing per-session writer leases. */\n\tasync list(options: SqliteSessionListOptions = {}): Promise<SqliteSessionMetadata[]> {\n\t\treturn this.operations.enqueue(async () => {\n\t\t\tconst path = await this.getDatabasePath();\n\t\t\tif (!resultOrThrow(await this.options.env.exists(path), `Failed to check database ${path}`)) return [];\n\t\t\tconst db = await this.getDatabase();\n\t\t\tconst rows = readSessionRows(db, options);\n\t\t\treturn rows.map((row) => decodeSessionMetadata(row, path));\n\t\t});\n\t}\n\n\tasync delete(metadata: SqliteSessionMetadata): Promise<void> {\n\t\treturn this.operations.enqueue(async () => {\n\t\t\tawait this.releaseStoragesForSession(metadata.id);\n\t\t\tconst db = await this.getDatabase();\n\t\t\tdb.transaction(() => {\n\t\t\t\tif (!sessionExists(db, metadata.id)) {\n\t\t\t\t\tdeleteWriterLease(db, metadata.id);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tclaimWriterLease(db, metadata.id, this.leaseOptions);\n\t\t\t\tdeleteBranchCache(db, metadata.id);\n\t\t\t\tdeleteFactRows(db, metadata.id);\n\t\t\t\tdeleteLaneRows(db, metadata.id);\n\t\t\t\tdeleteRecordRows(db, metadata.id);\n\t\t\t\tdeleteEntryRows(db, metadata.id);\n\t\t\t\tdeleteWriterLease(db, metadata.id);\n\t\t\t\tdeleteStats(db, metadata.id);\n\t\t\t\tdeleteSequence(db, metadata.id);\n\t\t\t\tdeleteSessionRow(db, metadata.id);\n\t\t\t});\n\t\t});\n\t}\n\n\tasync fork(\n\t\tsource: SqliteSessionMetadata,\n\t\toptions: ForkOptions & SqliteSessionCreateOptions,\n\t): Promise<Session<SqliteSessionMetadata>> {\n\t\treturn this.operations.enqueue(async () => {\n\t\t\tconst db = await this.getDatabase();\n\t\t\tconst path = await this.getDatabasePath();\n\t\t\tconst sourceMetadata = decodeSessionMetadata(requireSessionRow(db, source.id), path);\n\t\t\tconst id = options.id ?? uuidv7();\n\t\t\tif (sessionExists(db, id)) throw new SessionError(\"already_exists\", `Session already exists: ${id}`);\n\n\t\t\tconst entries: EntryRow[] = [];\n\t\t\tconst lanes: { lane: string; leafId: string | null }[] = [];\n\t\t\tconst branchTips: string[] = [];\n\t\t\tlet branchForkTargetId: string | null = null;\n\n\t\t\tif (options.scope === \"tree\") {\n\t\t\t\tentries.push(...readEntryRows(db, source.id, { order: \"oldestFirst\" }));\n\t\t\t\tlanes.push(...readLanes(db, source.id).map((row) => ({ lane: row.lane, leafId: row.leaf_id })));\n\t\t\t\tbranchTips.push(...readBranchTipIds(db, source.id));\n\t\t\t} else {\n\t\t\t\tconst main = readLane(db, source.id, \"main\");\n\t\t\t\tif (!main) throw new SessionError(\"invalid_lane\", \"Lane not found: main\");\n\t\t\t\tconst selectedEntryId = options.entryId ?? main.leaf_id;\n\t\t\t\tif (selectedEntryId !== null) {\n\t\t\t\t\tconst target = readEntryRow(db, source.id, selectedEntryId);\n\t\t\t\t\tif (!target || target.type !== \"message\") {\n\t\t\t\t\t\tthrow new SessionError(\n\t\t\t\t\t\t\t\"invalid_fork_target\",\n\t\t\t\t\t\t\t`Fork target is not a message entry: ${selectedEntryId}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst position = options.position ?? (options.entryId === undefined ? \"at\" : \"before\");\n\t\t\t\t\tbranchForkTargetId = position === \"at\" ? target.id : target.parent_id;\n\t\t\t\t}\n\t\t\t\tlanes.push({ lane: \"main\", leafId: branchForkTargetId });\n\t\t\t\tif (branchForkTargetId !== null) {\n\t\t\t\t\tconst cached = readCachedBranch(db, source.id, branchForkTargetId);\n\t\t\t\t\tif (!cached) {\n\t\t\t\t\t\tthrow new SessionError(\n\t\t\t\t\t\t\t\"invalid_fork_target\",\n\t\t\t\t\t\t\t`Fork target is not on a cached branch: ${branchForkTargetId}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst rows = queryCachedBranchRows(db, source.id, cached, { order: \"oldestFirst\" });\n\t\t\t\t\tentries.push(...rows.map(entryRowFromCached));\n\t\t\t\t\tbranchTips.push(branchForkTargetId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst copiedIds = new Set(entries.map((entry) => entry.id));\n\t\t\tconst latestName = readLatestFact(db, source.id, \"name\", null);\n\t\t\tconst latestLabels = readLatestLabelFacts(db, source.id);\n\t\t\tconst labelsToCopy = latestLabels.filter(\n\t\t\t\t(row) => options.scope === \"tree\" || (row.key !== null && copiedIds.has(row.key)),\n\t\t\t);\n\t\t\tconst createdAt = Date.now();\n\t\t\tconst metadata = options.metadata ?? sourceMetadata.metadata;\n\t\t\tlet lease: WriterLease;\n\n\t\t\ttry {\n\t\t\t\tlease = db.transaction(() => {\n\t\t\t\t\tinsertSessionRow(db, {\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tcreatedAt: timestampToText(createdAt),\n\t\t\t\t\t\tcwd: options.cwd,\n\t\t\t\t\t\tparentSessionId: options.parentSessionId ?? source.id,\n\t\t\t\t\t\tmetadata,\n\t\t\t\t\t});\n\t\t\t\t\tcreateSequence(db, id);\n\t\t\t\t\tcreateStats(db, id, entries.filter((entry) => entry.type === \"message\").length);\n\n\t\t\t\t\tlet nextSeq = 1;\n\t\t\t\t\tconst allocateSeq = () => nextSeq++;\n\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\tinsertEntryRow(db, id, {\n\t\t\t\t\t\t\tseq: allocateSeq(),\n\t\t\t\t\t\t\tid: entry.id,\n\t\t\t\t\t\t\tparentId: entry.parent_id,\n\t\t\t\t\t\t\ttype: entry.type,\n\t\t\t\t\t\t\ttimestamp: entry.timestamp,\n\t\t\t\t\t\t\tpayload: entry.payload,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (options.scope === \"tree\") {\n\t\t\t\t\t\tfor (const lane of lanes) insertLane(db, id, allocateSeq(), lane.lane, lane.leafId);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcreateInitialLane(db, id, \"main\", branchForkTargetId);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (latestName?.value !== undefined && latestName.value !== null) {\n\t\t\t\t\t\tappendFact(db, id, allocateSeq(), \"name\", null, latestName.value);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const label of labelsToCopy) appendFact(db, id, allocateSeq(), \"label\", label.key, label.value);\n\n\t\t\t\t\tsetNextSequence(db, id, nextSeq);\n\t\t\t\t\tfor (const tip of branchTips) buildCachedBranch(db, id, tip);\n\t\t\t\t\treturn claimWriterLease(db, id, this.leaseOptions);\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SessionError) throw error;\n\t\t\t\tthrow new SessionError(\n\t\t\t\t\t\"storage\",\n\t\t\t\t\t`Failed to fork SQLite session ${id}`,\n\t\t\t\t\terror instanceof Error ? error : undefined,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst row = requireSessionRow(db, id);\n\t\t\treturn this.sessionFromLease(db, decodeSessionMetadata(row, path), lease);\n\t\t});\n\t}\n\n\tasync close(): Promise<void> {\n\t\tawait this.operations.drain();\n\t\tfor (const storage of [...this.activeStorages]) await storage.release();\n\t\tif (this.database) this.database.close();\n\t\tthis.database = undefined;\n\t\tthis.databasePromise = undefined;\n\t}\n\n\tasync [Symbol.asyncDispose](): Promise<void> {\n\t\tawait this.close();\n\t}\n\n\tprivate async getDatabasePath(): Promise<string> {\n\t\tthis.databasePath ??= resultOrThrow(\n\t\t\tawait this.options.env.absolutePath(this.options.databasePath),\n\t\t\t`Failed to resolve SQLite sessions database ${this.options.databasePath}`,\n\t\t);\n\t\treturn this.databasePath;\n\t}\n\n\tprivate async getDatabase(): Promise<SqliteDatabase> {\n\t\tif (!this.databasePromise) this.databasePromise = this.openDatabase();\n\t\tthis.database = await this.databasePromise;\n\t\treturn this.database;\n\t}\n\n\tprivate async openDatabase(): Promise<SqliteDatabase> {\n\t\tconst path = await this.getDatabasePath();\n\t\tresultOrThrow(\n\t\t\tawait this.options.env.createDir(getParentPath(path), { recursive: true }),\n\t\t\t`Failed to create SQLite sessions directory ${path}`,\n\t\t);\n\t\tconst db = await this.options.sqlite.open(path);\n\t\ttry {\n\t\t\tconfigureSqliteDatabase(db);\n\t\t\tawait applyMigrations(db);\n\t\t\treturn db;\n\t\t} catch (error) {\n\t\t\tdb.close();\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"]}
|