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.
@@ -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 node of nodes.values()) {
240
- const relations = collectMaterializedLineageRows(node.sessionId, nodes, childrenByParent);
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
- const db = new Database(
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
- chunk_id UNINDEXED,
110
- session_id UNINDEXED,
111
- text
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, 2, ?, ?, ?, 12) as snippet,
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 = CAST(session_text_chunks_fts.chunk_id AS INTEGER)
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.prepare(
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
- const result = db
128
- .prepare(
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
- .run(
136
- row.sessionId,
137
- row.entryId ?? null,
138
- row.entryType,
139
- row.role ?? null,
140
- row.ts,
141
- row.sourceKind,
142
- row.text,
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.1",
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
- "dependencies": {
66
+ "optionalDependencies": {
68
67
  "better-sqlite3": "^12.9.0"
69
68
  }
70
69
  }