pi-sessions 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/extensions/session-ask.ts +297 -0
  4. package/extensions/session-auto-title/command.ts +109 -0
  5. package/extensions/session-auto-title/context.ts +41 -0
  6. package/extensions/session-auto-title/controller.ts +298 -0
  7. package/extensions/session-auto-title/model.ts +61 -0
  8. package/extensions/session-auto-title/prompt.ts +54 -0
  9. package/extensions/session-auto-title/retitle.ts +641 -0
  10. package/extensions/session-auto-title/state.ts +69 -0
  11. package/extensions/session-auto-title/wizard.ts +583 -0
  12. package/extensions/session-auto-title.ts +209 -0
  13. package/extensions/session-handoff/extract.ts +278 -0
  14. package/extensions/session-handoff/metadata.ts +96 -0
  15. package/extensions/session-handoff/picker.ts +394 -0
  16. package/extensions/session-handoff/query.ts +318 -0
  17. package/extensions/session-handoff/refs.ts +151 -0
  18. package/extensions/session-handoff/review.ts +268 -0
  19. package/extensions/session-handoff.ts +203 -0
  20. package/extensions/session-hooks.ts +55 -0
  21. package/extensions/session-index.ts +159 -0
  22. package/extensions/session-search/extract.ts +997 -0
  23. package/extensions/session-search/hooks.ts +350 -0
  24. package/extensions/session-search/normalize.ts +170 -0
  25. package/extensions/session-search/reindex.ts +93 -0
  26. package/extensions/session-search.ts +390 -0
  27. package/extensions/shared/search-snippet.ts +40 -0
  28. package/extensions/shared/session-index/common.ts +222 -0
  29. package/extensions/shared/session-index/index.ts +5 -0
  30. package/extensions/shared/session-index/lineage.ts +417 -0
  31. package/extensions/shared/session-index/schema.ts +178 -0
  32. package/extensions/shared/session-index/search.ts +688 -0
  33. package/extensions/shared/session-index/store.ts +173 -0
  34. package/extensions/shared/session-ui.ts +15 -0
  35. package/extensions/shared/settings.ts +141 -0
  36. package/extensions/shared/time.ts +38 -0
  37. package/extensions/shared/typebox.ts +61 -0
  38. package/images/handoff.png +0 -0
  39. package/images/session-title.png +0 -0
  40. package/images/session_ask.png +0 -0
  41. package/images/session_picker.png +0 -0
  42. package/images/session_search.png +0 -0
  43. package/package.json +64 -0
@@ -0,0 +1,350 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
3
+ import {
4
+ isToolCallEventType,
5
+ type ToolCallEvent,
6
+ type ToolResultEvent,
7
+ } from "@mariozechner/pi-coding-agent";
8
+ import {
9
+ clearSessionIndexedData,
10
+ getIndexStatus,
11
+ getSessionById,
12
+ INDEX_SCHEMA_VERSION,
13
+ insertSessionFileTouch,
14
+ insertTextChunk,
15
+ openIndexDatabase,
16
+ rebuildSessionLineageRelations,
17
+ type SessionLineageRow,
18
+ type SessionOrigin,
19
+ setMetadata,
20
+ upsertSession,
21
+ } from "../shared/session-index/index.js";
22
+ import { type ExtractedSessionRecord, extractSessionRecord } from "./extract.js";
23
+
24
+ const TOOL_RESULT_TEXT_LIMIT = 500;
25
+
26
+ type TrackedToolName = "read" | "edit" | "write";
27
+
28
+ export interface PendingToolCall {
29
+ toolCallId: string;
30
+ toolName: TrackedToolName;
31
+ path: string;
32
+ }
33
+
34
+ export interface FinalizedToolCall extends PendingToolCall {
35
+ isError: boolean;
36
+ resultText: string;
37
+ }
38
+
39
+ export interface SessionHookStateSnapshot {
40
+ currentSessionFile?: string | undefined;
41
+ currentCwd?: string | undefined;
42
+ pendingToolCalls: PendingToolCall[];
43
+ finalizedToolCalls: FinalizedToolCall[];
44
+ lastFlushedSessionFile?: string | undefined;
45
+ }
46
+
47
+ export interface SessionHookController {
48
+ getState(): SessionHookStateSnapshot;
49
+ handleSessionStart(sessionFile: string | undefined, cwd: string): Promise<boolean>;
50
+ handleSessionSwitch(
51
+ previousSessionFile: string | undefined,
52
+ sessionFile: string | undefined,
53
+ cwd: string,
54
+ sessionOrigin?: SessionOrigin,
55
+ ): Promise<boolean>;
56
+ handleSessionFork(
57
+ previousSessionFile: string | undefined,
58
+ sessionFile: string | undefined,
59
+ cwd: string,
60
+ ): Promise<boolean>;
61
+ handleToolCall(event: ToolCallEvent, sessionFile: string | undefined, cwd: string): void;
62
+ handleToolResult(event: ToolResultEvent): void;
63
+ handleTurnEnd(sessionFile: string | undefined, cwd: string): Promise<boolean>;
64
+ handleSessionTree(sessionFile: string | undefined, cwd: string): Promise<boolean>;
65
+ handleSessionCompact(sessionFile: string | undefined, cwd: string): Promise<boolean>;
66
+ handleSessionShutdown(sessionFile: string | undefined, cwd: string): Promise<boolean>;
67
+ }
68
+
69
+ interface SessionHookState {
70
+ currentSessionFile?: string | undefined;
71
+ currentCwd?: string | undefined;
72
+ pendingToolCalls: Map<string, PendingToolCall>;
73
+ finalizedToolCalls: Map<string, FinalizedToolCall>;
74
+ lastFlushedSessionFile?: string | undefined;
75
+ }
76
+
77
+ export function createSessionHookController(options: { indexPath: string }): SessionHookController {
78
+ const { indexPath } = options;
79
+ const state: SessionHookState = {
80
+ pendingToolCalls: new Map(),
81
+ finalizedToolCalls: new Map(),
82
+ };
83
+
84
+ return {
85
+ getState() {
86
+ return {
87
+ currentSessionFile: state.currentSessionFile,
88
+ currentCwd: state.currentCwd,
89
+ pendingToolCalls: [...state.pendingToolCalls.values()],
90
+ finalizedToolCalls: [...state.finalizedToolCalls.values()],
91
+ lastFlushedSessionFile: state.lastFlushedSessionFile,
92
+ };
93
+ },
94
+ async handleSessionStart(sessionFile, cwd) {
95
+ attachSession(state, sessionFile, cwd);
96
+ return syncAttachedSession(indexPath, state, "session_start");
97
+ },
98
+ async handleSessionSwitch(previousSessionFile, sessionFile, cwd, sessionOrigin) {
99
+ const previousSynced = syncSessionFile(indexPath, previousSessionFile, "session_switch");
100
+ attachSession(state, sessionFile, cwd);
101
+ const currentSynced = syncAttachedSession(indexPath, state, "session_switch", sessionOrigin);
102
+ return previousSynced || currentSynced;
103
+ },
104
+ async handleSessionFork(previousSessionFile, sessionFile, cwd) {
105
+ const previousSynced = syncSessionFile(indexPath, previousSessionFile, "session_fork");
106
+ attachSession(state, sessionFile, cwd);
107
+ const currentSynced = syncAttachedSession(indexPath, state, "session_fork", "fork");
108
+ return previousSynced || currentSynced;
109
+ },
110
+ handleToolCall(event, sessionFile, cwd) {
111
+ attachSession(state, sessionFile, cwd);
112
+ const pendingToolCall = buildPendingToolCall(event);
113
+ if (!pendingToolCall) {
114
+ return;
115
+ }
116
+
117
+ state.pendingToolCalls.set(event.toolCallId, pendingToolCall);
118
+ },
119
+ handleToolResult(event) {
120
+ const pendingToolCall = state.pendingToolCalls.get(event.toolCallId);
121
+ if (!pendingToolCall) {
122
+ return;
123
+ }
124
+
125
+ state.pendingToolCalls.delete(event.toolCallId);
126
+ state.finalizedToolCalls.set(event.toolCallId, {
127
+ ...pendingToolCall,
128
+ isError: event.isError,
129
+ resultText: summarizeToolResultText(event.toolName, event.content),
130
+ });
131
+ },
132
+ async handleTurnEnd(sessionFile, cwd) {
133
+ attachSession(state, sessionFile, cwd);
134
+ const synced = syncAttachedSession(indexPath, state, "turn_end");
135
+ clearTurnState(state);
136
+ return synced;
137
+ },
138
+ async handleSessionTree(sessionFile, cwd) {
139
+ attachSession(state, sessionFile, cwd);
140
+ return syncAttachedSession(indexPath, state, "session_tree");
141
+ },
142
+ async handleSessionCompact(sessionFile, cwd) {
143
+ attachSession(state, sessionFile, cwd);
144
+ return syncAttachedSession(indexPath, state, "session_compact");
145
+ },
146
+ async handleSessionShutdown(sessionFile, cwd) {
147
+ attachSession(state, sessionFile, cwd);
148
+ const synced = syncAttachedSession(indexPath, state, "session_shutdown");
149
+ clearTurnState(state);
150
+ state.currentSessionFile = undefined;
151
+ state.currentCwd = undefined;
152
+ return synced;
153
+ },
154
+ };
155
+ }
156
+
157
+ function attachSession(
158
+ state: SessionHookState,
159
+ sessionFile: string | undefined,
160
+ cwd: string,
161
+ ): void {
162
+ if (sessionFile !== undefined) {
163
+ state.currentSessionFile = sessionFile;
164
+ }
165
+
166
+ state.currentCwd = cwd;
167
+ }
168
+
169
+ function syncAttachedSession(
170
+ indexPath: string,
171
+ state: SessionHookState,
172
+ eventType: string,
173
+ sessionOrigin?: SessionOrigin,
174
+ ): boolean {
175
+ return syncSessionFile(indexPath, state.currentSessionFile, eventType, state, sessionOrigin);
176
+ }
177
+
178
+ function syncSessionFile(
179
+ indexPath: string,
180
+ sessionFile: string | undefined,
181
+ eventType: string,
182
+ state?: SessionHookState,
183
+ sessionOrigin?: SessionOrigin,
184
+ ): boolean {
185
+ if (!sessionFile || !existsSync(sessionFile)) {
186
+ return false;
187
+ }
188
+
189
+ const status = getIndexStatus(indexPath);
190
+ if (!status.exists || status.schemaVersion !== INDEX_SCHEMA_VERSION) {
191
+ return false;
192
+ }
193
+
194
+ const extracted = extractSessionRecord(sessionFile);
195
+ if (!extracted) {
196
+ return false;
197
+ }
198
+
199
+ const db = openIndexDatabase(indexPath, { create: false });
200
+ try {
201
+ db.transaction(() => {
202
+ const existingSession = getSessionById(db, extracted.sessionId);
203
+ const sessionRow = mergeSessionLineage(extracted, existingSession, sessionOrigin);
204
+ upsertSession(db, sessionRow, "hook");
205
+ if (shouldRefreshLineageRelations(existingSession, sessionRow)) {
206
+ rebuildSessionLineageRelations(db);
207
+ }
208
+ clearSessionIndexedData(db, extracted.sessionId);
209
+
210
+ for (const chunk of extracted.chunks) {
211
+ insertTextChunk(db, { sessionId: extracted.sessionId, ...chunk });
212
+ }
213
+
214
+ for (const fileTouch of extracted.fileTouches) {
215
+ insertSessionFileTouch(db, { sessionId: extracted.sessionId, ...fileTouch });
216
+ }
217
+
218
+ setMetadata(db, "hook_updated_at", new Date().toISOString());
219
+ setMetadata(db, "hook_last_event", eventType);
220
+ })();
221
+ } finally {
222
+ db.close();
223
+ }
224
+
225
+ if (state) {
226
+ state.lastFlushedSessionFile = sessionFile;
227
+ }
228
+
229
+ return true;
230
+ }
231
+
232
+ function mergeSessionLineage(
233
+ extracted: ExtractedSessionRecord,
234
+ existing: SessionLineageRow | undefined,
235
+ sessionOrigin?: SessionOrigin,
236
+ ): ExtractedSessionRecord {
237
+ const parentSessionPath = extracted.parentSessionPath ?? existing?.parentSessionPath;
238
+ const parentSessionId = extracted.parentSessionId ?? existing?.parentSessionId;
239
+ const nextOrigin = resolveSessionOrigin(
240
+ sessionOrigin,
241
+ extracted.sessionOrigin,
242
+ existing?.sessionOrigin,
243
+ );
244
+
245
+ return {
246
+ ...extracted,
247
+ parentSessionPath,
248
+ parentSessionId,
249
+ sessionOrigin: parentSessionPath ? (nextOrigin ?? "unknown_child") : undefined,
250
+ };
251
+ }
252
+
253
+ function resolveSessionOrigin(
254
+ explicit: SessionOrigin | undefined,
255
+ extracted: SessionOrigin | undefined,
256
+ existing: SessionOrigin | undefined,
257
+ ): SessionOrigin | undefined {
258
+ if (explicit) {
259
+ return explicit;
260
+ }
261
+
262
+ // Preserve a specific origin when the extracted record only knows "unknown_child"
263
+ if (extracted === "unknown_child" && existing && existing !== "unknown_child") {
264
+ return existing;
265
+ }
266
+
267
+ return extracted ?? existing;
268
+ }
269
+
270
+ function shouldRefreshLineageRelations(
271
+ existing: SessionLineageRow | undefined,
272
+ next: ExtractedSessionRecord,
273
+ ): boolean {
274
+ if (!existing) {
275
+ return true;
276
+ }
277
+
278
+ return (
279
+ existing.sessionPath !== next.sessionPath ||
280
+ existing.parentSessionPath !== next.parentSessionPath ||
281
+ existing.parentSessionId !== next.parentSessionId
282
+ );
283
+ }
284
+
285
+ function buildPendingToolCall(event: ToolCallEvent): PendingToolCall | undefined {
286
+ if (isToolCallEventType("read", event)) {
287
+ return buildTrackedToolCall(event.toolCallId, "read", event.input.path);
288
+ }
289
+
290
+ if (isToolCallEventType("edit", event)) {
291
+ return buildTrackedToolCall(event.toolCallId, "edit", event.input.path);
292
+ }
293
+
294
+ if (isToolCallEventType("write", event)) {
295
+ return buildTrackedToolCall(event.toolCallId, "write", event.input.path);
296
+ }
297
+
298
+ return undefined;
299
+ }
300
+
301
+ function buildTrackedToolCall(
302
+ toolCallId: string,
303
+ toolName: TrackedToolName,
304
+ rawPath: string,
305
+ ): PendingToolCall | undefined {
306
+ const path = stringValue(rawPath);
307
+ if (!path) {
308
+ return undefined;
309
+ }
310
+
311
+ return {
312
+ toolCallId,
313
+ toolName,
314
+ path,
315
+ };
316
+ }
317
+
318
+ function summarizeToolResultText(
319
+ toolName: string,
320
+ content: Array<TextContent | ImageContent>,
321
+ ): string {
322
+ const text = content
323
+ .filter((part): part is TextContent => part.type === "text" && typeof part.text === "string")
324
+ .map((part) => part.text)
325
+ .join("\n")
326
+ .trim();
327
+
328
+ if (toolName === "write") {
329
+ return text;
330
+ }
331
+
332
+ return truncateText(text, TOOL_RESULT_TEXT_LIMIT);
333
+ }
334
+
335
+ function truncateText(text: string, limit: number): string {
336
+ if (text.length <= limit) {
337
+ return text;
338
+ }
339
+
340
+ return `${text.slice(0, limit)}…`;
341
+ }
342
+
343
+ function stringValue(value: unknown): string | undefined {
344
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
345
+ }
346
+
347
+ function clearTurnState(state: SessionHookState): void {
348
+ state.pendingToolCalls.clear();
349
+ state.finalizedToolCalls.clear();
350
+ }
@@ -0,0 +1,170 @@
1
+ import { existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export type FileTouchOp = "changed" | "read" | "touched";
6
+ export type FileTouchSource = "tool_call" | "branch_summary_details" | "compaction_details";
7
+ export type PathScope = "absolute" | "relative" | "basename";
8
+
9
+ export interface NormalizedPathRecord {
10
+ rawPath: string;
11
+ absPath?: string | undefined;
12
+ cwdRelPath?: string | undefined;
13
+ repoRoot?: string | undefined;
14
+ repoRelPath?: string | undefined;
15
+ basename: string;
16
+ pathScope: PathScope;
17
+ op?: FileTouchOp | undefined;
18
+ }
19
+
20
+ export function normalizePathRecord(rawPath: string, cwd: string): NormalizedPathRecord {
21
+ const trimmedPath = rawPath.trim();
22
+ const expandedPath = expandHome(trimmedPath);
23
+ const pathScope = classifyPathScope(expandedPath);
24
+ const absPath = resolveAbsolutePath(expandedPath, cwd);
25
+ const repoRoot = absPath ? deriveRepoRootForPath(absPath) : undefined;
26
+
27
+ return {
28
+ rawPath: trimmedPath,
29
+ absPath,
30
+ cwdRelPath: absPath ? toRelativePath(absPath, cwd) : undefined,
31
+ repoRoot,
32
+ repoRelPath: absPath && repoRoot ? toRelativePath(absPath, repoRoot) : undefined,
33
+ basename: deriveBasename(trimmedPath, absPath),
34
+ pathScope,
35
+ };
36
+ }
37
+
38
+ export function deriveRepoRootForPath(absPath: string): string | undefined {
39
+ let current = path.normalize(absPath);
40
+
41
+ while (true) {
42
+ if (existsSync(path.join(current, ".git"))) {
43
+ return current;
44
+ }
45
+
46
+ const parent = path.dirname(current);
47
+ if (parent === current) {
48
+ return undefined;
49
+ }
50
+
51
+ current = parent;
52
+ }
53
+ }
54
+
55
+ export function deriveSessionRepoRoots(
56
+ cwd: string,
57
+ normalizedPaths: NormalizedPathRecord[],
58
+ ): string[] {
59
+ const repoRoots = new Set<string>();
60
+ if (cwd && path.isAbsolute(cwd)) {
61
+ const cwdRepoRoot = deriveRepoRootForPath(cwd);
62
+ if (cwdRepoRoot) {
63
+ repoRoots.add(cwdRepoRoot);
64
+ }
65
+ }
66
+
67
+ for (const normalizedPath of normalizedPaths) {
68
+ if (!normalizedPath.repoRoot || normalizedPath.op === "read") {
69
+ continue;
70
+ }
71
+
72
+ repoRoots.add(normalizedPath.repoRoot);
73
+ }
74
+
75
+ return [...repoRoots].sort();
76
+ }
77
+
78
+ export function matchesRepoRoot(repoRoot: string, repoQuery: string): boolean {
79
+ const normalizedRepoRoot = path.normalize(repoRoot);
80
+ const normalizedQuery = normalizeSearchPath(repoQuery);
81
+ if (!normalizedQuery) {
82
+ return false;
83
+ }
84
+
85
+ if (path.isAbsolute(normalizedQuery) || normalizedQuery.includes("/")) {
86
+ return normalizedRepoRoot === normalizedQuery || normalizedRepoRoot.endsWith(normalizedQuery);
87
+ }
88
+
89
+ return path.basename(normalizedRepoRoot) === normalizedQuery;
90
+ }
91
+
92
+ export function normalizeSearchPath(rawPath: string): string | undefined {
93
+ const trimmedPath = rawPath.trim();
94
+ if (!trimmedPath) {
95
+ return undefined;
96
+ }
97
+
98
+ const expandedPath = expandHome(trimmedPath);
99
+ return path.normalize(expandedPath).replace(/\\/g, "/");
100
+ }
101
+
102
+ function classifyPathScope(rawPath: string): PathScope {
103
+ if (path.isAbsolute(rawPath)) {
104
+ return "absolute";
105
+ }
106
+
107
+ if (rawPath.includes("/") || rawPath.includes("\\") || rawPath === "." || rawPath === "..") {
108
+ return "relative";
109
+ }
110
+
111
+ return "basename";
112
+ }
113
+
114
+ function expandHome(rawPath: string): string {
115
+ if (rawPath === "~") {
116
+ return os.homedir();
117
+ }
118
+
119
+ if (rawPath.startsWith(`~${path.sep}`) || rawPath.startsWith("~/")) {
120
+ return path.join(os.homedir(), rawPath.slice(2));
121
+ }
122
+
123
+ return rawPath;
124
+ }
125
+
126
+ function resolveAbsolutePath(rawPath: string, cwd: string): string | undefined {
127
+ if (!cwd || !path.isAbsolute(cwd)) {
128
+ return path.isAbsolute(rawPath) ? normalizeAbsolute(rawPath) : undefined;
129
+ }
130
+
131
+ if (path.isAbsolute(rawPath)) {
132
+ return normalizeAbsolute(rawPath);
133
+ }
134
+
135
+ return normalizeAbsolute(path.resolve(cwd, rawPath));
136
+ }
137
+
138
+ function normalizeAbsolute(absPath: string): string {
139
+ return path.normalize(absPath).replace(/\\/g, "/");
140
+ }
141
+
142
+ function toRelativePath(absPath: string, root: string): string | undefined {
143
+ if (!root || !path.isAbsolute(root)) {
144
+ return undefined;
145
+ }
146
+
147
+ const normalizedAbsPath = normalizeAbsolute(absPath);
148
+ const normalizedRoot = normalizeAbsolute(root);
149
+ if (!isWithinPath(normalizedAbsPath, normalizedRoot)) {
150
+ return undefined;
151
+ }
152
+
153
+ const relativePath = path.relative(normalizedRoot, normalizedAbsPath).replace(/\\/g, "/");
154
+ return relativePath || ".";
155
+ }
156
+
157
+ function isWithinPath(targetPath: string, basePath: string): boolean {
158
+ if (targetPath === basePath) {
159
+ return true;
160
+ }
161
+
162
+ return targetPath.startsWith(`${basePath}/`);
163
+ }
164
+
165
+ function deriveBasename(rawPath: string, absPath?: string): string {
166
+ const targetPath = absPath ?? rawPath;
167
+ const normalizedPath = targetPath.replace(/[\\/]+$/, "");
168
+ const basename = path.basename(normalizedPath);
169
+ return basename || rawPath;
170
+ }
@@ -0,0 +1,93 @@
1
+ import { renameSync } from "node:fs";
2
+ import { SessionManager } from "@mariozechner/pi-coding-agent";
3
+ import {
4
+ createTempIndexPath,
5
+ initializeSchema,
6
+ insertSession,
7
+ insertSessionFileTouch,
8
+ insertTextChunk,
9
+ openIndexDatabase,
10
+ rebuildSessionLineageRelations,
11
+ type SessionIndexDatabase,
12
+ setMetadata,
13
+ } from "../shared/session-index/index.js";
14
+ import { extractSessionRecord, type SearchTextChunk, type SessionFileTouch } from "./extract.js";
15
+
16
+ export interface ReindexOptions {
17
+ indexPath: string;
18
+ }
19
+
20
+ export interface ReindexResult {
21
+ sessionCount: number;
22
+ chunkCount: number;
23
+ indexPath: string;
24
+ }
25
+
26
+ export async function rebuildSessionIndex(options: ReindexOptions): Promise<ReindexResult> {
27
+ const finalIndexPath = options.indexPath;
28
+ const tempIndexPath = createTempIndexPath(finalIndexPath);
29
+ const sessionFiles = (await SessionManager.listAll()).map((session) => session.path);
30
+
31
+ const db = openIndexDatabase(tempIndexPath, { create: true });
32
+ let sessionCount: number;
33
+ let chunkCount: number;
34
+ try {
35
+ initializeSchema(db);
36
+ ({ sessionCount, chunkCount } = indexSessionFiles(db, sessionFiles));
37
+ } finally {
38
+ db.close();
39
+ }
40
+
41
+ renameSync(tempIndexPath, finalIndexPath);
42
+ return { sessionCount, chunkCount, indexPath: finalIndexPath };
43
+ }
44
+
45
+ function indexSessionFiles(
46
+ db: SessionIndexDatabase,
47
+ sessionFiles: string[],
48
+ ): { sessionCount: number; chunkCount: number } {
49
+ return db.transaction((files: string[]) => {
50
+ let sessionCount = 0;
51
+ let chunkCount = 0;
52
+
53
+ for (const sessionFile of files) {
54
+ const extracted = extractSessionRecord(sessionFile);
55
+ if (!extracted) {
56
+ continue;
57
+ }
58
+
59
+ insertSession(db, extracted, "full_reindex");
60
+ sessionCount += 1;
61
+ chunkCount += insertSessionChunks(db, extracted.sessionId, extracted.chunks);
62
+ insertSessionFileTouches(db, extracted.sessionId, extracted.fileTouches);
63
+ }
64
+
65
+ rebuildSessionLineageRelations(db);
66
+ setMetadata(db, "indexed_at", new Date().toISOString());
67
+ setMetadata(db, "session_source", "SessionManager.listAll()");
68
+
69
+ return { sessionCount, chunkCount };
70
+ })(sessionFiles);
71
+ }
72
+
73
+ function insertSessionChunks(
74
+ db: SessionIndexDatabase,
75
+ sessionId: string,
76
+ chunks: SearchTextChunk[],
77
+ ): number {
78
+ for (const chunk of chunks) {
79
+ insertTextChunk(db, { sessionId, ...chunk });
80
+ }
81
+
82
+ return chunks.length;
83
+ }
84
+
85
+ function insertSessionFileTouches(
86
+ db: SessionIndexDatabase,
87
+ sessionId: string,
88
+ fileTouches: SessionFileTouch[],
89
+ ): void {
90
+ for (const fileTouch of fileTouches) {
91
+ insertSessionFileTouch(db, { sessionId, ...fileTouch });
92
+ }
93
+ }