pi-sessions 0.3.1 → 0.4.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/extensions/session-handoff/extract.ts +15 -14
- package/extensions/session-handoff.ts +2 -1
- package/extensions/session-search/extract.ts +218 -50
- package/extensions/session-search/hooks.ts +253 -103
- package/extensions/session-search/reindex.ts +48 -50
- package/extensions/shared/session-index/common.ts +6 -3
- package/extensions/shared/session-index/lineage.ts +72 -4
- package/extensions/shared/session-index/schema.ts +27 -23
- package/extensions/shared/session-index/search.ts +2 -2
- package/extensions/shared/session-index/sqlite.ts +98 -0
- package/extensions/shared/session-index/store.ts +125 -33
- package/package.json +3 -4
|
@@ -113,7 +113,7 @@ export function getSessionById(
|
|
|
113
113
|
)
|
|
114
114
|
.get(sessionId);
|
|
115
115
|
|
|
116
|
-
if (row === undefined) {
|
|
116
|
+
if (row === undefined || row === null) {
|
|
117
117
|
return undefined;
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -140,7 +140,7 @@ export function getSessionByPath(
|
|
|
140
140
|
)
|
|
141
141
|
.get(sessionPath);
|
|
142
142
|
|
|
143
|
-
if (row === undefined) {
|
|
143
|
+
if (row === undefined || row === null) {
|
|
144
144
|
return undefined;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -185,9 +185,39 @@ export function getSiblingSessions(
|
|
|
185
185
|
return queryRelatedSessions(db, sessionId, ["sibling"]);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
interface SessionGraph {
|
|
189
|
+
nodes: Map<string, SessionGraphNode>;
|
|
190
|
+
childrenByParent: Map<string, string[]>;
|
|
191
|
+
}
|
|
192
|
+
|
|
188
193
|
export function rebuildSessionLineageRelations(db: SessionIndexDatabase): void {
|
|
189
194
|
db.prepare(`DELETE FROM session_lineage_relations`).run();
|
|
190
195
|
|
|
196
|
+
const graph = buildSessionGraph(db);
|
|
197
|
+
insertLineageRelationRows(db, graph.nodes.keys(), graph);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Recomputes lineage for just the connected component(s) containing the seed
|
|
201
|
+
// sessions. Lineage relations only ever link sessions connected through parent
|
|
202
|
+
// edges, so the rest of the table is untouched.
|
|
203
|
+
export function refreshSessionLineageRelationsFor(
|
|
204
|
+
db: SessionIndexDatabase,
|
|
205
|
+
seedSessionIds: Array<string | undefined>,
|
|
206
|
+
): void {
|
|
207
|
+
const graph = buildSessionGraph(db);
|
|
208
|
+
const component = collectLineageComponent(seedSessionIds, graph);
|
|
209
|
+
if (component.size === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const placeholders = [...component].map(() => "?").join(", ");
|
|
214
|
+
db.prepare(`DELETE FROM session_lineage_relations WHERE session_id IN (${placeholders})`).run(
|
|
215
|
+
...component,
|
|
216
|
+
);
|
|
217
|
+
insertLineageRelationRows(db, component, graph);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildSessionGraph(db: SessionIndexDatabase): SessionGraph {
|
|
191
221
|
const rows = parseTypeBoxRows(
|
|
192
222
|
SESSION_GRAPH_ROW_SCHEMA,
|
|
193
223
|
db
|
|
@@ -229,6 +259,40 @@ export function rebuildSessionLineageRelations(db: SessionIndexDatabase): void {
|
|
|
229
259
|
childrenByParent.set(node.resolvedParentSessionId, children);
|
|
230
260
|
}
|
|
231
261
|
|
|
262
|
+
return { nodes, childrenByParent };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function collectLineageComponent(
|
|
266
|
+
seedSessionIds: Array<string | undefined>,
|
|
267
|
+
graph: SessionGraph,
|
|
268
|
+
): Set<string> {
|
|
269
|
+
const component = new Set<string>();
|
|
270
|
+
const queue = seedSessionIds.filter(
|
|
271
|
+
(sessionId): sessionId is string => sessionId !== undefined && graph.nodes.has(sessionId),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
while (queue.length > 0) {
|
|
275
|
+
const sessionId = queue.pop();
|
|
276
|
+
if (!sessionId || component.has(sessionId)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
component.add(sessionId);
|
|
281
|
+
const parentId = graph.nodes.get(sessionId)?.resolvedParentSessionId;
|
|
282
|
+
if (parentId) {
|
|
283
|
+
queue.push(parentId);
|
|
284
|
+
}
|
|
285
|
+
queue.push(...(graph.childrenByParent.get(sessionId) ?? []));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return component;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function insertLineageRelationRows(
|
|
292
|
+
db: SessionIndexDatabase,
|
|
293
|
+
sessionIds: Iterable<string>,
|
|
294
|
+
graph: SessionGraph,
|
|
295
|
+
): void {
|
|
232
296
|
const insertRelation = db.prepare(
|
|
233
297
|
`
|
|
234
298
|
INSERT INTO session_lineage_relations(session_id, related_session_id, relation, distance)
|
|
@@ -236,8 +300,12 @@ export function rebuildSessionLineageRelations(db: SessionIndexDatabase): void {
|
|
|
236
300
|
`,
|
|
237
301
|
);
|
|
238
302
|
|
|
239
|
-
for (const
|
|
240
|
-
const relations = collectMaterializedLineageRows(
|
|
303
|
+
for (const sessionId of sessionIds) {
|
|
304
|
+
const relations = collectMaterializedLineageRows(
|
|
305
|
+
sessionId,
|
|
306
|
+
graph.nodes,
|
|
307
|
+
graph.childrenByParent,
|
|
308
|
+
);
|
|
241
309
|
for (const relation of relations.values()) {
|
|
242
310
|
insertRelation.run(
|
|
243
311
|
relation.sessionId,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import Database from "better-sqlite3";
|
|
4
2
|
import { parseTypeBoxValue } from "../typebox.js";
|
|
5
3
|
import {
|
|
6
4
|
INDEX_SCHEMA_VERSION,
|
|
@@ -9,6 +7,7 @@ import {
|
|
|
9
7
|
type SessionIndexDatabase,
|
|
10
8
|
type SessionIndexStatus,
|
|
11
9
|
} from "./common.js";
|
|
10
|
+
import { openSqlite } from "./sqlite.js";
|
|
12
11
|
|
|
13
12
|
export function ensureIndexDir(dir: string): string {
|
|
14
13
|
if (!existsSync(dir)) {
|
|
@@ -17,28 +16,12 @@ export function ensureIndexDir(dir: string): string {
|
|
|
17
16
|
return dir;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function createTempIndexPath(finalPath: string): string {
|
|
21
|
-
const dir = path.dirname(finalPath);
|
|
22
|
-
const baseName = path.basename(finalPath, path.extname(finalPath));
|
|
23
|
-
ensureIndexDir(dir);
|
|
24
|
-
return path.join(dir, `${baseName}.tmp-${process.pid}-${Date.now()}.sqlite`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
export function openIndexDatabase(
|
|
28
20
|
dbPath: string,
|
|
29
21
|
options?: { create?: boolean; timeoutMs?: number },
|
|
30
22
|
): SessionIndexDatabase {
|
|
31
23
|
const create = options?.create ?? true;
|
|
32
|
-
|
|
33
|
-
dbPath,
|
|
34
|
-
options?.timeoutMs === undefined
|
|
35
|
-
? { fileMustExist: !create }
|
|
36
|
-
: { fileMustExist: !create, timeout: options.timeoutMs },
|
|
37
|
-
);
|
|
38
|
-
db.pragma("journal_mode = WAL");
|
|
39
|
-
db.pragma("synchronous = NORMAL");
|
|
40
|
-
db.pragma("foreign_keys = ON");
|
|
41
|
-
return db;
|
|
24
|
+
return openSqlite(dbPath, { create, timeoutMs: options?.timeoutMs });
|
|
42
25
|
}
|
|
43
26
|
|
|
44
27
|
export function initializeSchema(db: SessionIndexDatabase): void {
|
|
@@ -64,6 +47,9 @@ export function initializeSchema(db: SessionIndexDatabase): void {
|
|
|
64
47
|
session_origin TEXT,
|
|
65
48
|
handoff_goal TEXT,
|
|
66
49
|
handoff_next_task TEXT,
|
|
50
|
+
indexed_file_size INTEGER,
|
|
51
|
+
indexed_file_mtime_ms INTEGER,
|
|
52
|
+
indexed_file_anchor TEXT,
|
|
67
53
|
index_version INTEGER NOT NULL,
|
|
68
54
|
indexed_at_ts TEXT NOT NULL,
|
|
69
55
|
index_source TEXT NOT NULL
|
|
@@ -106,11 +92,29 @@ export function initializeSchema(db: SessionIndexDatabase): void {
|
|
|
106
92
|
CREATE INDEX IF NOT EXISTS session_text_chunks_ts_idx ON session_text_chunks(ts DESC);
|
|
107
93
|
|
|
108
94
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_text_chunks_fts USING fts5(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
text,
|
|
96
|
+
content='session_text_chunks',
|
|
97
|
+
content_rowid='id'
|
|
112
98
|
);
|
|
113
99
|
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_ai
|
|
101
|
+
AFTER INSERT ON session_text_chunks BEGIN
|
|
102
|
+
INSERT INTO session_text_chunks_fts(rowid, text) VALUES (new.id, new.text);
|
|
103
|
+
END;
|
|
104
|
+
|
|
105
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_ad
|
|
106
|
+
AFTER DELETE ON session_text_chunks BEGIN
|
|
107
|
+
INSERT INTO session_text_chunks_fts(session_text_chunks_fts, rowid, text)
|
|
108
|
+
VALUES ('delete', old.id, old.text);
|
|
109
|
+
END;
|
|
110
|
+
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_au
|
|
112
|
+
AFTER UPDATE ON session_text_chunks BEGIN
|
|
113
|
+
INSERT INTO session_text_chunks_fts(session_text_chunks_fts, rowid, text)
|
|
114
|
+
VALUES ('delete', old.id, old.text);
|
|
115
|
+
INSERT INTO session_text_chunks_fts(rowid, text) VALUES (new.id, new.text);
|
|
116
|
+
END;
|
|
117
|
+
|
|
114
118
|
CREATE TABLE IF NOT EXISTS session_file_touches (
|
|
115
119
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
120
|
session_id TEXT NOT NULL,
|
|
@@ -148,7 +152,7 @@ export function setMetadata(db: SessionIndexDatabase, key: string, value: string
|
|
|
148
152
|
|
|
149
153
|
export function getMetadata(db: SessionIndexDatabase, key: string): string | undefined {
|
|
150
154
|
const row = db.prepare(`SELECT value FROM metadata WHERE key = ?`).get(key);
|
|
151
|
-
if (row === undefined) {
|
|
155
|
+
if (row === undefined || row === null) {
|
|
152
156
|
return undefined;
|
|
153
157
|
}
|
|
154
158
|
|
|
@@ -325,12 +325,12 @@ function getTextMatchRows(db: SessionIndexDatabase, filters: SearchFilters): Sea
|
|
|
325
325
|
s.session_origin as sessionOrigin,
|
|
326
326
|
s.handoff_goal as handoffGoal,
|
|
327
327
|
s.handoff_next_task as handoffNextTask,
|
|
328
|
-
snippet(session_text_chunks_fts,
|
|
328
|
+
snippet(session_text_chunks_fts, 0, ?, ?, ?, 12) as snippet,
|
|
329
329
|
bm25(session_text_chunks_fts) as rank,
|
|
330
330
|
c.entry_id as entryId,
|
|
331
331
|
c.source_kind as sourceKind
|
|
332
332
|
FROM session_text_chunks_fts
|
|
333
|
-
JOIN session_text_chunks c ON c.id =
|
|
333
|
+
JOIN session_text_chunks c ON c.id = session_text_chunks_fts.rowid
|
|
334
334
|
JOIN sessions s ON s.session_id = c.session_id
|
|
335
335
|
WHERE session_text_chunks_fts MATCH ?
|
|
336
336
|
AND (? IS NULL OR s.modified_ts >= ?)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const requireModule = createRequire(import.meta.url);
|
|
4
|
+
const isBun = Boolean(process.versions.bun);
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
7
|
+
|
|
8
|
+
export type SqliteBindValue = string | number | bigint | null;
|
|
9
|
+
|
|
10
|
+
export interface SqliteRunResult {
|
|
11
|
+
changes: number;
|
|
12
|
+
lastInsertRowid: number | bigint;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SqliteStatement {
|
|
16
|
+
get(...params: SqliteBindValue[]): unknown;
|
|
17
|
+
all(...params: SqliteBindValue[]): unknown[];
|
|
18
|
+
run(...params: SqliteBindValue[]): SqliteRunResult;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SqliteTransaction<Args extends unknown[], Result> {
|
|
22
|
+
(...args: Args): Result;
|
|
23
|
+
immediate(...args: Args): Result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SqliteDatabase {
|
|
27
|
+
prepare(sql: string): SqliteStatement;
|
|
28
|
+
exec(sql: string): void;
|
|
29
|
+
transaction<Args extends unknown[], Result>(
|
|
30
|
+
fn: (...args: Args) => Result,
|
|
31
|
+
): SqliteTransaction<Args, Result>;
|
|
32
|
+
close(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SqliteConstructor {
|
|
36
|
+
new (path: string, options?: Record<string, unknown>): SqliteDatabase;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The driver is resolved at runtime so Bun never loads the native better-sqlite3
|
|
40
|
+
// addon (unsupported) and Node never loads the bun:sqlite builtin. The require
|
|
41
|
+
// result is untyped, so the cast to our structural contract is unavoidable here.
|
|
42
|
+
function loadDatabaseConstructor(): SqliteConstructor {
|
|
43
|
+
if (isBun) {
|
|
44
|
+
return (requireModule("bun:sqlite") as { Database: SqliteConstructor }).Database;
|
|
45
|
+
}
|
|
46
|
+
return requireModule("better-sqlite3") as SqliteConstructor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function openSqlite(
|
|
50
|
+
dbPath: string,
|
|
51
|
+
options: { create: boolean; timeoutMs?: number | undefined },
|
|
52
|
+
): SqliteDatabase {
|
|
53
|
+
const Database = loadDatabaseConstructor();
|
|
54
|
+
const db = isBun
|
|
55
|
+
? new Database(dbPath, { create: options.create, readwrite: true })
|
|
56
|
+
: new Database(dbPath, { fileMustExist: !options.create });
|
|
57
|
+
|
|
58
|
+
// busy_timeout must be set before any other statement: the journal_mode
|
|
59
|
+
// pragma below is the connection's first lock acquisition (and may run WAL
|
|
60
|
+
// recovery), and immediate transactions rely on this timeout to queue behind
|
|
61
|
+
// concurrent writers instead of failing.
|
|
62
|
+
db.exec(`PRAGMA busy_timeout = ${options.timeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
63
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
64
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
65
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
66
|
+
|
|
67
|
+
return withStatementCache(db);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Neither driver memoizes prepare(), and the write paths prepare the same
|
|
71
|
+
// statements once per row. SQLite re-prepares cached statements transparently
|
|
72
|
+
// when the schema changes, so reusing them across DDL is safe.
|
|
73
|
+
function withStatementCache(db: SqliteDatabase): SqliteDatabase {
|
|
74
|
+
const statements = new Map<string, SqliteStatement>();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
prepare(sql) {
|
|
78
|
+
const cached = statements.get(sql);
|
|
79
|
+
if (cached) {
|
|
80
|
+
return cached;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const statement = db.prepare(sql);
|
|
84
|
+
statements.set(sql, statement);
|
|
85
|
+
return statement;
|
|
86
|
+
},
|
|
87
|
+
exec(sql) {
|
|
88
|
+
db.exec(sql);
|
|
89
|
+
},
|
|
90
|
+
transaction(fn) {
|
|
91
|
+
return db.transaction(fn);
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
statements.clear();
|
|
95
|
+
db.close();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,12 +1,38 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { parseTypeBoxValue } from "../typebox.js";
|
|
1
3
|
import {
|
|
2
4
|
compactSessionId,
|
|
3
5
|
INDEX_SCHEMA_VERSION,
|
|
6
|
+
NULLABLE_STRING_SCHEMA,
|
|
7
|
+
parseRepoRoots,
|
|
8
|
+
SESSION_ORIGIN_SCHEMA,
|
|
4
9
|
type SessionFileTouchRow,
|
|
5
10
|
type SessionIndexDatabase,
|
|
6
11
|
type SessionRow,
|
|
7
12
|
type SessionTextChunkRow,
|
|
8
13
|
} from "./common.js";
|
|
9
14
|
|
|
15
|
+
const SESSION_ROW_QUERY_SCHEMA = Type.Object({
|
|
16
|
+
sessionId: Type.String(),
|
|
17
|
+
sessionPath: Type.String(),
|
|
18
|
+
sessionName: Type.String(),
|
|
19
|
+
firstUserPrompt: NULLABLE_STRING_SCHEMA,
|
|
20
|
+
cwd: Type.String(),
|
|
21
|
+
repoRootsJson: Type.String(),
|
|
22
|
+
startedAt: Type.String(),
|
|
23
|
+
modifiedAt: Type.String(),
|
|
24
|
+
messageCount: Type.Number(),
|
|
25
|
+
entryCount: Type.Number(),
|
|
26
|
+
parentSessionPath: NULLABLE_STRING_SCHEMA,
|
|
27
|
+
parentSessionId: NULLABLE_STRING_SCHEMA,
|
|
28
|
+
sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
|
|
29
|
+
handoffGoal: NULLABLE_STRING_SCHEMA,
|
|
30
|
+
handoffNextTask: NULLABLE_STRING_SCHEMA,
|
|
31
|
+
indexedFileSize: Type.Union([Type.Number(), Type.Null()]),
|
|
32
|
+
indexedFileMtimeMs: Type.Union([Type.Number(), Type.Null()]),
|
|
33
|
+
indexedFileAnchor: NULLABLE_STRING_SCHEMA,
|
|
34
|
+
});
|
|
35
|
+
|
|
10
36
|
function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
11
37
|
return [
|
|
12
38
|
row.sessionId,
|
|
@@ -24,6 +50,9 @@ function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
|
24
50
|
row.sessionOrigin ?? null,
|
|
25
51
|
row.handoffGoal ?? null,
|
|
26
52
|
row.handoffNextTask ?? null,
|
|
53
|
+
row.indexedFileSize ?? null,
|
|
54
|
+
row.indexedFileMtimeMs ?? null,
|
|
55
|
+
row.indexedFileAnchor ?? null,
|
|
27
56
|
INDEX_SCHEMA_VERSION,
|
|
28
57
|
new Date().toISOString(),
|
|
29
58
|
indexSource,
|
|
@@ -42,8 +71,9 @@ export function insertSession(
|
|
|
42
71
|
created_ts, modified_ts, message_count, entry_count,
|
|
43
72
|
parent_session_path, parent_session_id, session_origin,
|
|
44
73
|
handoff_goal, handoff_next_task,
|
|
74
|
+
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
45
75
|
index_version, indexed_at_ts, index_source
|
|
46
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
77
|
`,
|
|
48
78
|
).run(...sessionRowBindings(row, indexSource));
|
|
49
79
|
insertSessionIdChunk(db, row);
|
|
@@ -61,8 +91,9 @@ export function upsertSession(
|
|
|
61
91
|
created_ts, modified_ts, message_count, entry_count,
|
|
62
92
|
parent_session_path, parent_session_id, session_origin,
|
|
63
93
|
handoff_goal, handoff_next_task,
|
|
94
|
+
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
64
95
|
index_version, indexed_at_ts, index_source
|
|
65
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
97
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
67
98
|
session_path = excluded.session_path,
|
|
68
99
|
session_name = excluded.session_name,
|
|
@@ -78,6 +109,9 @@ export function upsertSession(
|
|
|
78
109
|
session_origin = excluded.session_origin,
|
|
79
110
|
handoff_goal = excluded.handoff_goal,
|
|
80
111
|
handoff_next_task = excluded.handoff_next_task,
|
|
112
|
+
indexed_file_size = excluded.indexed_file_size,
|
|
113
|
+
indexed_file_mtime_ms = excluded.indexed_file_mtime_ms,
|
|
114
|
+
indexed_file_anchor = excluded.indexed_file_anchor,
|
|
81
115
|
index_version = excluded.index_version,
|
|
82
116
|
indexed_at_ts = excluded.indexed_at_ts,
|
|
83
117
|
index_source = excluded.index_source
|
|
@@ -86,6 +120,72 @@ export function upsertSession(
|
|
|
86
120
|
syncSessionIdChunk(db, row);
|
|
87
121
|
}
|
|
88
122
|
|
|
123
|
+
export function getSessionRowByPath(
|
|
124
|
+
db: SessionIndexDatabase,
|
|
125
|
+
sessionPath: string,
|
|
126
|
+
): SessionRow | undefined {
|
|
127
|
+
const row = db
|
|
128
|
+
.prepare(
|
|
129
|
+
`
|
|
130
|
+
SELECT
|
|
131
|
+
session_id as sessionId,
|
|
132
|
+
session_path as sessionPath,
|
|
133
|
+
session_name as sessionName,
|
|
134
|
+
first_user_prompt as firstUserPrompt,
|
|
135
|
+
cwd,
|
|
136
|
+
repo_roots_json as repoRootsJson,
|
|
137
|
+
created_ts as startedAt,
|
|
138
|
+
modified_ts as modifiedAt,
|
|
139
|
+
message_count as messageCount,
|
|
140
|
+
entry_count as entryCount,
|
|
141
|
+
parent_session_path as parentSessionPath,
|
|
142
|
+
parent_session_id as parentSessionId,
|
|
143
|
+
session_origin as sessionOrigin,
|
|
144
|
+
handoff_goal as handoffGoal,
|
|
145
|
+
handoff_next_task as handoffNextTask,
|
|
146
|
+
indexed_file_size as indexedFileSize,
|
|
147
|
+
indexed_file_mtime_ms as indexedFileMtimeMs,
|
|
148
|
+
indexed_file_anchor as indexedFileAnchor
|
|
149
|
+
FROM sessions
|
|
150
|
+
WHERE session_path = ?
|
|
151
|
+
ORDER BY indexed_at_ts DESC
|
|
152
|
+
LIMIT 1
|
|
153
|
+
`,
|
|
154
|
+
)
|
|
155
|
+
.get(sessionPath);
|
|
156
|
+
|
|
157
|
+
if (row === undefined || row === null) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return buildSessionRow(
|
|
162
|
+
parseTypeBoxValue(SESSION_ROW_QUERY_SCHEMA, row, `Invalid session row for path ${sessionPath}`),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildSessionRow(row: Static<typeof SESSION_ROW_QUERY_SCHEMA>): SessionRow {
|
|
167
|
+
return {
|
|
168
|
+
sessionId: row.sessionId,
|
|
169
|
+
sessionPath: row.sessionPath,
|
|
170
|
+
sessionName: row.sessionName,
|
|
171
|
+
firstUserPrompt: row.firstUserPrompt ?? undefined,
|
|
172
|
+
cwd: row.cwd,
|
|
173
|
+
repoRoots: parseRepoRoots(row.repoRootsJson),
|
|
174
|
+
startedAt: row.startedAt,
|
|
175
|
+
modifiedAt: row.modifiedAt,
|
|
176
|
+
messageCount: row.messageCount,
|
|
177
|
+
entryCount: row.entryCount,
|
|
178
|
+
parentSessionPath: row.parentSessionPath ?? undefined,
|
|
179
|
+
parentSessionId: row.parentSessionId ?? undefined,
|
|
180
|
+
sessionOrigin: row.sessionOrigin ?? undefined,
|
|
181
|
+
handoffGoal: row.handoffGoal ?? undefined,
|
|
182
|
+
handoffNextTask: row.handoffNextTask ?? undefined,
|
|
183
|
+
indexedFileSize: row.indexedFileSize ?? undefined,
|
|
184
|
+
indexedFileMtimeMs: row.indexedFileMtimeMs ?? undefined,
|
|
185
|
+
indexedFileAnchor: row.indexedFileAnchor ?? undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
89
189
|
function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
90
190
|
insertTextChunk(db, {
|
|
91
191
|
sessionId: row.sessionId,
|
|
@@ -97,18 +197,7 @@ function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
|
97
197
|
}
|
|
98
198
|
|
|
99
199
|
function syncSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
100
|
-
db.
|
|
101
|
-
`DELETE FROM session_text_chunks_fts
|
|
102
|
-
WHERE chunk_id IN (
|
|
103
|
-
SELECT CAST(id AS TEXT) FROM session_text_chunks
|
|
104
|
-
WHERE session_id = ? AND source_kind = 'session_id'
|
|
105
|
-
)`,
|
|
106
|
-
).run(row.sessionId);
|
|
107
|
-
|
|
108
|
-
db.prepare(
|
|
109
|
-
`DELETE FROM session_text_chunks WHERE session_id = ? AND source_kind = 'session_id'`,
|
|
110
|
-
).run(row.sessionId);
|
|
111
|
-
|
|
200
|
+
clearSessionChunksBySourceKind(db, row.sessionId, "session_id");
|
|
112
201
|
insertSessionIdChunk(db, row);
|
|
113
202
|
}
|
|
114
203
|
|
|
@@ -117,35 +206,38 @@ function buildSessionIdSearchText(sessionId: string): string {
|
|
|
117
206
|
return compact === sessionId ? sessionId : `${sessionId} ${compact}`;
|
|
118
207
|
}
|
|
119
208
|
|
|
209
|
+
export function clearSessionChunksBySourceKind(
|
|
210
|
+
db: SessionIndexDatabase,
|
|
211
|
+
sessionId: string,
|
|
212
|
+
sourceKind: string,
|
|
213
|
+
): void {
|
|
214
|
+
db.prepare(`DELETE FROM session_text_chunks WHERE session_id = ? AND source_kind = ?`).run(
|
|
215
|
+
sessionId,
|
|
216
|
+
sourceKind,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
120
220
|
export function clearSessionIndexedData(db: SessionIndexDatabase, sessionId: string): void {
|
|
121
|
-
db.prepare(`DELETE FROM session_text_chunks_fts WHERE session_id = ?`).run(sessionId);
|
|
122
221
|
db.prepare(`DELETE FROM session_text_chunks WHERE session_id = ?`).run(sessionId);
|
|
123
222
|
db.prepare(`DELETE FROM session_file_touches WHERE session_id = ?`).run(sessionId);
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
export function insertTextChunk(db: SessionIndexDatabase, row: SessionTextChunkRow): void {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
226
|
+
db.prepare(
|
|
227
|
+
`
|
|
130
228
|
INSERT INTO session_text_chunks(
|
|
131
229
|
session_id, entry_id, entry_type, role, ts, source_kind, text
|
|
132
230
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
133
231
|
`,
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const chunkId = Number(result.lastInsertRowid);
|
|
146
|
-
db.prepare(
|
|
147
|
-
`INSERT INTO session_text_chunks_fts(chunk_id, session_id, text) VALUES (?, ?, ?)`,
|
|
148
|
-
).run(chunkId, row.sessionId, row.text);
|
|
232
|
+
).run(
|
|
233
|
+
row.sessionId,
|
|
234
|
+
row.entryId ?? null,
|
|
235
|
+
row.entryType,
|
|
236
|
+
row.role ?? null,
|
|
237
|
+
row.ts,
|
|
238
|
+
row.sourceKind,
|
|
239
|
+
row.text,
|
|
240
|
+
);
|
|
149
241
|
}
|
|
150
242
|
|
|
151
243
|
export function insertSessionFileTouch(db: SessionIndexDatabase, row: SessionFileTouchRow): void {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"check": "biome check . && tsc --noEmit && vitest run",
|
|
41
|
-
"prepare": "husky"
|
|
41
|
+
"prepare": "husky || true"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@biomejs/biome": "^2.4.14",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@earendil-works/pi-ai": "^0.78.1",
|
|
47
47
|
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
48
48
|
"@earendil-works/pi-tui": "^0.78.1",
|
|
49
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
50
49
|
"@types/node": "^25.6.2",
|
|
51
50
|
"husky": "^9.1.7",
|
|
52
51
|
"lint-staged": "^17.0.3",
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
"lint-staged": {
|
|
65
64
|
"*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|
|
66
65
|
},
|
|
67
|
-
"
|
|
66
|
+
"optionalDependencies": {
|
|
68
67
|
"better-sqlite3": "^12.9.0"
|
|
69
68
|
}
|
|
70
69
|
}
|