pi-sessions 0.5.1 → 0.7.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 (57) hide show
  1. package/README.md +22 -1
  2. package/extensions/session-ask/agent.ts +400 -0
  3. package/extensions/session-ask/navigate.ts +880 -0
  4. package/extensions/session-ask.ts +82 -134
  5. package/extensions/session-auto-title/command.ts +1 -1
  6. package/extensions/session-auto-title/controller.ts +3 -3
  7. package/extensions/session-auto-title/generate.ts +2 -2
  8. package/extensions/session-auto-title/model.ts +4 -12
  9. package/extensions/session-auto-title/retitle.ts +5 -5
  10. package/extensions/session-auto-title/state.ts +1 -1
  11. package/extensions/session-auto-title/wizard.ts +4 -4
  12. package/extensions/session-auto-title.ts +8 -8
  13. package/extensions/session-handoff/extract.ts +18 -5
  14. package/extensions/session-handoff/metadata.ts +4 -1
  15. package/extensions/session-handoff/picker.ts +52 -6
  16. package/extensions/session-handoff/query.ts +78 -62
  17. package/extensions/session-handoff/refs.ts +14 -20
  18. package/extensions/session-handoff/spawn.ts +1 -1
  19. package/extensions/session-handoff.ts +54 -35
  20. package/extensions/session-hooks.ts +3 -3
  21. package/extensions/session-index.ts +4 -4
  22. package/extensions/session-messaging/broker/process.ts +302 -0
  23. package/extensions/session-messaging/broker/spawn.ts +164 -0
  24. package/extensions/session-messaging/pi/incoming-runtime.ts +114 -0
  25. package/extensions/session-messaging/pi/message-contracts.ts +36 -0
  26. package/extensions/session-messaging/pi/message-view.ts +131 -0
  27. package/extensions/session-messaging/pi/renderer.ts +16 -0
  28. package/extensions/session-messaging/pi/service.ts +370 -0
  29. package/extensions/session-messaging/pi/tools.ts +92 -0
  30. package/extensions/session-messaging.ts +30 -0
  31. package/extensions/session-search/extract.ts +90 -440
  32. package/extensions/session-search/hooks.ts +20 -28
  33. package/extensions/session-search/normalize.ts +1 -1
  34. package/extensions/session-search/reindex.ts +3 -2
  35. package/extensions/session-search.ts +161 -132
  36. package/extensions/shared/model.ts +18 -0
  37. package/extensions/shared/session-broker/active.ts +26 -0
  38. package/extensions/shared/session-broker/client.ts +253 -0
  39. package/extensions/shared/session-broker/framing.ts +72 -0
  40. package/extensions/shared/session-broker/protocol.ts +95 -0
  41. package/extensions/shared/session-broker/socket-path.ts +30 -0
  42. package/extensions/shared/session-index/access.ts +128 -0
  43. package/extensions/shared/session-index/common.ts +46 -51
  44. package/extensions/shared/session-index/index.ts +6 -5
  45. package/extensions/shared/session-index/lineage.ts +19 -2
  46. package/extensions/shared/session-index/query/ast.ts +40 -0
  47. package/extensions/shared/session-index/query/compiler.ts +146 -0
  48. package/extensions/shared/session-index/query/lexer.ts +140 -0
  49. package/extensions/shared/session-index/query/parser.ts +178 -0
  50. package/extensions/shared/session-index/schema.ts +25 -9
  51. package/extensions/shared/session-index/scoring.ts +80 -0
  52. package/extensions/shared/session-index/search.ts +555 -281
  53. package/extensions/shared/session-index/sqlite.ts +16 -9
  54. package/extensions/shared/session-index/store.ts +14 -23
  55. package/extensions/shared/settings.ts +62 -5
  56. package/extensions/shared/text.ts +50 -0
  57. package/package.json +4 -3
@@ -0,0 +1,253 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import net from "node:net";
4
+ import { readFrames, writeFrame } from "./framing.ts";
5
+ import { BROKER_FRAME_SCHEMA, type SessionMessagingBrokerFrame } from "./protocol.ts";
6
+ import { getSessionMessagingSocketPath } from "./socket-path.ts";
7
+
8
+ export const INCOMING_SESSION_MESSAGE_EVENT = "session_messaging.incoming_message";
9
+
10
+ const REQUEST_TIMEOUT_MS = 10_000;
11
+ const SOCKET_PATH = getSessionMessagingSocketPath();
12
+
13
+ interface PendingListRequest {
14
+ resolve(sessionIds: string[]): void;
15
+ reject(error: Error): void;
16
+ timeout: NodeJS.Timeout;
17
+ }
18
+
19
+ interface PendingSendRequest {
20
+ resolve(result: SessionMessageSendResult): void;
21
+ reject(error: Error): void;
22
+ timeout: NodeJS.Timeout;
23
+ }
24
+
25
+ export interface SessionMessageSendResult {
26
+ messageId: string;
27
+ delivered: boolean;
28
+ error?: string | undefined;
29
+ }
30
+
31
+ export interface SendSessionMessageOptions {
32
+ messageId: string;
33
+ target: string;
34
+ body: string;
35
+ sentAt: string;
36
+ requestResponse?: boolean | undefined;
37
+ sourceToolCallId?: string | undefined;
38
+ }
39
+
40
+ export class SessionMessagingClient extends EventEmitter {
41
+ private socket: net.Socket | undefined;
42
+ private sessionId: string | undefined;
43
+ private pendingLists = new Map<string, PendingListRequest>();
44
+ private pendingSends = new Map<string, PendingSendRequest>();
45
+
46
+ get isConnected(): boolean {
47
+ return Boolean(this.socket && !this.socket.destroyed && this.socket.writable && this.sessionId);
48
+ }
49
+
50
+ get registeredSessionId(): string | undefined {
51
+ return this.sessionId;
52
+ }
53
+
54
+ async connect(sessionId: string): Promise<void> {
55
+ if (this.isConnected) {
56
+ return;
57
+ }
58
+
59
+ await new Promise<void>((resolve, reject) => {
60
+ const socket = net.connect(SOCKET_PATH);
61
+ this.socket = socket;
62
+ let settled = false;
63
+
64
+ const timeout = setTimeout(() => {
65
+ if (settled) return;
66
+ settled = true;
67
+ cleanup();
68
+ socket.destroy();
69
+ reject(new Error("Session messaging broker connection timed out."));
70
+ }, REQUEST_TIMEOUT_MS);
71
+
72
+ const cleanup = (): void => {
73
+ clearTimeout(timeout);
74
+ socket.off("error", onError);
75
+ socket.off("close", onClose);
76
+ this.off("registered", onRegistered);
77
+ this.off("register_failed", onRegisterFailed);
78
+ };
79
+
80
+ const fail = (error: Error): void => {
81
+ if (settled) return;
82
+ settled = true;
83
+ cleanup();
84
+ socket.destroy();
85
+ reject(error);
86
+ };
87
+
88
+ const onError = (error: Error): void => fail(error);
89
+ const onClose = (): void =>
90
+ fail(new Error("Session messaging broker closed before registration."));
91
+ const onRegistered = (): void => {
92
+ if (settled) return;
93
+ settled = true;
94
+ cleanup();
95
+ socket.on("close", () => {
96
+ if (this.socket === socket) {
97
+ this.cleanup();
98
+ this.emit("disconnect");
99
+ }
100
+ });
101
+ resolve();
102
+ };
103
+ const onRegisterFailed = (reason: string): void => fail(new Error(reason));
104
+
105
+ socket.on("error", onError);
106
+ socket.on("close", onClose);
107
+ this.once("registered", onRegistered);
108
+ this.once("register_failed", onRegisterFailed);
109
+ void this.readSocket(socket);
110
+
111
+ socket.setKeepAlive(true, 5_000);
112
+ writeFrame(socket, { type: "register", sessionId });
113
+ });
114
+ }
115
+
116
+ disconnect(): void {
117
+ if (this.socket && !this.socket.destroyed) {
118
+ if (!this.socket.writableEnded) {
119
+ try {
120
+ writeFrame(this.socket, { type: "unregister" });
121
+ } catch {}
122
+ }
123
+ this.socket.end();
124
+ }
125
+ this.cleanup();
126
+ }
127
+
128
+ listSessionIds(): Promise<string[]> {
129
+ const socket = this.requireSocket();
130
+ const requestId = randomUUID();
131
+
132
+ return new Promise((resolve, reject) => {
133
+ const timeout = setTimeout(() => {
134
+ this.pendingLists.delete(requestId);
135
+ reject(new Error("Session live list timed out."));
136
+ }, REQUEST_TIMEOUT_MS);
137
+ this.pendingLists.set(requestId, { resolve, reject, timeout });
138
+ writeFrame(socket, { type: "list", requestId });
139
+ });
140
+ }
141
+
142
+ sendMessage(options: SendSessionMessageOptions): Promise<SessionMessageSendResult> {
143
+ const socket = this.requireSocket();
144
+ const requestId = randomUUID();
145
+
146
+ return new Promise((resolve, reject) => {
147
+ const timeout = setTimeout(() => {
148
+ this.pendingSends.delete(requestId);
149
+ reject(new Error("Session message send timed out."));
150
+ }, REQUEST_TIMEOUT_MS + 1_000);
151
+ this.pendingSends.set(requestId, { resolve, reject, timeout });
152
+ writeFrame(socket, {
153
+ type: "send",
154
+ requestId,
155
+ messageId: options.messageId,
156
+ target: options.target,
157
+ body: options.body,
158
+ requestResponse: options.requestResponse,
159
+ sourceToolCallId: options.sourceToolCallId,
160
+ sentAt: options.sentAt,
161
+ });
162
+ });
163
+ }
164
+
165
+ acknowledgeIncoming(
166
+ requestId: string,
167
+ messageId: string,
168
+ result: { delivered: true } | { delivered: false; error: string },
169
+ ): void {
170
+ const socket = this.requireSocket();
171
+ writeFrame(socket, {
172
+ type: "incoming_ack",
173
+ requestId,
174
+ messageId,
175
+ delivered: result.delivered,
176
+ error: result.delivered ? undefined : result.error,
177
+ });
178
+ }
179
+
180
+ private async readSocket(socket: net.Socket): Promise<void> {
181
+ try {
182
+ for await (const frame of readFrames(
183
+ socket,
184
+ BROKER_FRAME_SCHEMA,
185
+ "Invalid session messaging broker frame",
186
+ )) {
187
+ this.handleFrame(frame);
188
+ }
189
+ } catch (error) {
190
+ socket.destroy(error instanceof Error ? error : new Error(String(error)));
191
+ }
192
+ }
193
+
194
+ private handleFrame(frame: SessionMessagingBrokerFrame): void {
195
+ switch (frame.type) {
196
+ case "registered":
197
+ this.sessionId = frame.sessionId;
198
+ this.emit("registered");
199
+ break;
200
+ case "register_failed":
201
+ this.emit("register_failed", frame.reason);
202
+ break;
203
+ case "sessions": {
204
+ const pending = this.pendingLists.get(frame.requestId);
205
+ if (!pending) return;
206
+ clearTimeout(pending.timeout);
207
+ this.pendingLists.delete(frame.requestId);
208
+ pending.resolve(frame.sessionIds);
209
+ break;
210
+ }
211
+ case "incoming":
212
+ this.emit(INCOMING_SESSION_MESSAGE_EVENT, frame.requestId, frame.message);
213
+ break;
214
+ case "send_result": {
215
+ const pending = this.pendingSends.get(frame.requestId);
216
+ if (!pending) return;
217
+ clearTimeout(pending.timeout);
218
+ this.pendingSends.delete(frame.requestId);
219
+ pending.resolve({
220
+ messageId: frame.messageId,
221
+ delivered: frame.delivered,
222
+ error: frame.error,
223
+ });
224
+ break;
225
+ }
226
+ case "error":
227
+ this.socket?.destroy(new Error(frame.message));
228
+ break;
229
+ }
230
+ }
231
+
232
+ private requireSocket(): net.Socket {
233
+ if (!this.socket || this.socket.destroyed || !this.socket.writable || !this.sessionId) {
234
+ throw new Error("Session messaging is not connected.");
235
+ }
236
+
237
+ return this.socket;
238
+ }
239
+
240
+ private cleanup(): void {
241
+ this.sessionId = undefined;
242
+ for (const pending of this.pendingLists.values()) {
243
+ clearTimeout(pending.timeout);
244
+ pending.reject(new Error("Session messaging disconnected."));
245
+ }
246
+ this.pendingLists.clear();
247
+ for (const pending of this.pendingSends.values()) {
248
+ clearTimeout(pending.timeout);
249
+ pending.reject(new Error("Session messaging disconnected."));
250
+ }
251
+ this.pendingSends.clear();
252
+ }
253
+ }
@@ -0,0 +1,72 @@
1
+ import type { Socket } from "node:net";
2
+ import { createInterface } from "node:readline";
3
+ import type { Static, TSchema } from "typebox";
4
+ import { parseTypeBoxValue } from "../typebox.ts";
5
+
6
+ const MAX_FRAME_BYTES = 256 * 1024;
7
+
8
+ export function writeFrame<T>(socket: Socket, frame: T): void {
9
+ socket.write(`${JSON.stringify(frame)}\n`);
10
+ }
11
+
12
+ export async function* readFrames<T extends TSchema>(
13
+ socket: Socket,
14
+ schema: T,
15
+ context: string,
16
+ ): AsyncGenerator<Static<T>> {
17
+ const reader = createInterface({ input: socket, crlfDelay: Number.POSITIVE_INFINITY });
18
+ const lines: string[] = [];
19
+ const waiters: Array<() => void> = [];
20
+ let error: Error | undefined;
21
+ let closed = false;
22
+
23
+ const wake = (): void => {
24
+ const waiter = waiters.shift();
25
+ waiter?.();
26
+ };
27
+
28
+ reader.on("line", (line) => {
29
+ lines.push(line);
30
+ wake();
31
+ });
32
+ reader.on("error", (nextError) => {
33
+ error = nextError instanceof Error ? nextError : new Error(String(nextError));
34
+ closed = true;
35
+ wake();
36
+ });
37
+ reader.on("close", () => {
38
+ closed = true;
39
+ wake();
40
+ });
41
+
42
+ try {
43
+ while (true) {
44
+ while (lines.length === 0 && !closed) {
45
+ await new Promise<void>((resolve) => waiters.push(resolve));
46
+ }
47
+
48
+ const line = lines.shift();
49
+ if (line === undefined) {
50
+ if (error) {
51
+ throw error;
52
+ }
53
+ return;
54
+ }
55
+
56
+ if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
57
+ throw new Error("Session messaging frame exceeds maximum size.");
58
+ }
59
+
60
+ let parsed: unknown;
61
+ try {
62
+ parsed = JSON.parse(line) as unknown;
63
+ } catch (parseError) {
64
+ throw parseError instanceof Error ? parseError : new Error(String(parseError));
65
+ }
66
+
67
+ yield parseTypeBoxValue(schema, parsed, context);
68
+ }
69
+ } finally {
70
+ reader.close();
71
+ }
72
+ }
@@ -0,0 +1,95 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
4
+ messageId: Type.String(),
5
+ source: Type.String(),
6
+ target: Type.String(),
7
+ body: Type.String(),
8
+ requestResponse: Type.Optional(Type.Boolean()),
9
+ sentAt: Type.String(),
10
+ sourceToolCallId: Type.Optional(Type.String()),
11
+ });
12
+
13
+ const REGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
14
+ type: Type.Literal("register"),
15
+ sessionId: Type.String(),
16
+ });
17
+ const UNREGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
18
+ type: Type.Literal("unregister"),
19
+ });
20
+ const LIST_CLIENT_FRAME_SCHEMA = Type.Object({
21
+ type: Type.Literal("list"),
22
+ requestId: Type.String(),
23
+ });
24
+ const SEND_CLIENT_FRAME_SCHEMA = Type.Object({
25
+ type: Type.Literal("send"),
26
+ requestId: Type.String(),
27
+ messageId: Type.String(),
28
+ target: Type.String(),
29
+ body: Type.String(),
30
+ requestResponse: Type.Optional(Type.Boolean()),
31
+ sentAt: Type.String(),
32
+ sourceToolCallId: Type.Optional(Type.String()),
33
+ });
34
+ const INCOMING_ACK_CLIENT_FRAME_SCHEMA = Type.Object({
35
+ type: Type.Literal("incoming_ack"),
36
+ requestId: Type.String(),
37
+ messageId: Type.String(),
38
+ delivered: Type.Boolean(),
39
+ error: Type.Optional(Type.String()),
40
+ });
41
+
42
+ const REGISTERED_BROKER_FRAME_SCHEMA = Type.Object({
43
+ type: Type.Literal("registered"),
44
+ sessionId: Type.String(),
45
+ });
46
+ const REGISTER_FAILED_BROKER_FRAME_SCHEMA = Type.Object({
47
+ type: Type.Literal("register_failed"),
48
+ reason: Type.String(),
49
+ });
50
+ const SESSIONS_BROKER_FRAME_SCHEMA = Type.Object({
51
+ type: Type.Literal("sessions"),
52
+ requestId: Type.String(),
53
+ sessionIds: Type.Array(Type.String()),
54
+ });
55
+ const INCOMING_BROKER_FRAME_SCHEMA = Type.Object({
56
+ type: Type.Literal("incoming"),
57
+ requestId: Type.String(),
58
+ message: SESSION_MESSAGE_PAYLOAD_SCHEMA,
59
+ });
60
+ const SEND_RESULT_BROKER_FRAME_SCHEMA = Type.Object({
61
+ type: Type.Literal("send_result"),
62
+ requestId: Type.String(),
63
+ messageId: Type.String(),
64
+ delivered: Type.Boolean(),
65
+ error: Type.Optional(Type.String()),
66
+ });
67
+ const ERROR_BROKER_FRAME_SCHEMA = Type.Object({
68
+ type: Type.Literal("error"),
69
+ message: Type.String(),
70
+ });
71
+
72
+ export const CLIENT_FRAME_SCHEMA = Type.Union([
73
+ REGISTER_CLIENT_FRAME_SCHEMA,
74
+ UNREGISTER_CLIENT_FRAME_SCHEMA,
75
+ LIST_CLIENT_FRAME_SCHEMA,
76
+ SEND_CLIENT_FRAME_SCHEMA,
77
+ INCOMING_ACK_CLIENT_FRAME_SCHEMA,
78
+ ]);
79
+
80
+ export const BROKER_FRAME_SCHEMA = Type.Union([
81
+ REGISTERED_BROKER_FRAME_SCHEMA,
82
+ REGISTER_FAILED_BROKER_FRAME_SCHEMA,
83
+ SESSIONS_BROKER_FRAME_SCHEMA,
84
+ INCOMING_BROKER_FRAME_SCHEMA,
85
+ SEND_RESULT_BROKER_FRAME_SCHEMA,
86
+ ERROR_BROKER_FRAME_SCHEMA,
87
+ ]);
88
+
89
+ export type SessionMessagePayload = Static<typeof SESSION_MESSAGE_PAYLOAD_SCHEMA>;
90
+ export type SessionMessagingSendClientFrame = Static<typeof SEND_CLIENT_FRAME_SCHEMA>;
91
+ export type SessionMessagingIncomingAckClientFrame = Static<
92
+ typeof INCOMING_ACK_CLIENT_FRAME_SCHEMA
93
+ >;
94
+ export type SessionMessagingClientFrame = Static<typeof CLIENT_FRAME_SCHEMA>;
95
+ export type SessionMessagingBrokerFrame = Static<typeof BROKER_FRAME_SCHEMA>;
@@ -0,0 +1,30 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ function sanitizePipeSegment(value: string): string {
5
+ return (
6
+ value
7
+ .replace(/[^a-zA-Z0-9]+/g, "-")
8
+ .replace(/^-+|-+$/g, "")
9
+ .toLowerCase() || "default"
10
+ );
11
+ }
12
+
13
+ export function getSessionMessagingDir(homeDir: string = homedir()): string {
14
+ return (
15
+ process.env.PI_SESSIONS_MESSAGING_DIR ??
16
+ join(homeDir, ".pi", "agent", "pi-sessions", "messaging")
17
+ );
18
+ }
19
+
20
+ export function getSessionMessagingSocketPath(
21
+ platform: NodeJS.Platform = process.platform,
22
+ homeDir: string = homedir(),
23
+ ): string {
24
+ const messagingDir = getSessionMessagingDir(homeDir);
25
+ if (platform === "win32") {
26
+ return `\\\\.\\pipe\\pi-sessions-messaging-${sanitizePipeSegment(messagingDir)}`;
27
+ }
28
+
29
+ return join(messagingDir, "broker.sock");
30
+ }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { parseTypeBoxValue } from "../typebox.ts";
3
+ import {
4
+ INDEX_SCHEMA_VERSION,
5
+ ROW_COUNT_SCHEMA,
6
+ type SessionIndexDatabase,
7
+ type SessionIndexStatus,
8
+ } from "./common.ts";
9
+ import { getMetadata, openIndexDatabase } from "./schema.ts";
10
+
11
+ export type SessionIndexOpenMode = "read" | "write";
12
+
13
+ export interface WithSessionIndexOptions {
14
+ mode: SessionIndexOpenMode;
15
+ required: boolean;
16
+ timeoutMs?: number | undefined;
17
+ }
18
+
19
+ export interface ValidSessionIndex {
20
+ db: SessionIndexDatabase;
21
+ status: SessionIndexStatus;
22
+ }
23
+
24
+ export function withSessionIndex<T>(
25
+ indexPath: string,
26
+ options: WithSessionIndexOptions & { required: true },
27
+ read: (index: ValidSessionIndex) => T,
28
+ ): T;
29
+ export function withSessionIndex<T>(
30
+ indexPath: string,
31
+ options: WithSessionIndexOptions & { required: false },
32
+ read: (index: ValidSessionIndex) => T,
33
+ ): T | undefined;
34
+ export function withSessionIndex<T>(
35
+ indexPath: string,
36
+ options: WithSessionIndexOptions,
37
+ read: (index: ValidSessionIndex) => T,
38
+ ): T | undefined {
39
+ const opened = openValidatedSessionIndexInternal(indexPath, options);
40
+ if (!opened) {
41
+ return undefined;
42
+ }
43
+
44
+ try {
45
+ return read(opened);
46
+ } finally {
47
+ opened.db.close();
48
+ }
49
+ }
50
+
51
+ export function openValidatedSessionIndex(
52
+ indexPath: string,
53
+ options: WithSessionIndexOptions & { required: true },
54
+ ): ValidSessionIndex;
55
+ export function openValidatedSessionIndex(
56
+ indexPath: string,
57
+ options: WithSessionIndexOptions & { required: false },
58
+ ): ValidSessionIndex | undefined;
59
+ export function openValidatedSessionIndex(
60
+ indexPath: string,
61
+ options: WithSessionIndexOptions,
62
+ ): ValidSessionIndex | undefined {
63
+ return openValidatedSessionIndexInternal(indexPath, options);
64
+ }
65
+
66
+ function openValidatedSessionIndexInternal(
67
+ indexPath: string,
68
+ options: WithSessionIndexOptions,
69
+ ): ValidSessionIndex | undefined {
70
+ if (!existsSync(indexPath)) {
71
+ return handleUnavailableIndex(indexPath, options);
72
+ }
73
+
74
+ let db: SessionIndexDatabase | undefined;
75
+ try {
76
+ db = openIndexDatabase(indexPath, {
77
+ create: false,
78
+ mode: options.mode,
79
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
80
+ });
81
+ const status = getIndexStatusFromDb(indexPath, db);
82
+ if (status.schemaVersion !== INDEX_SCHEMA_VERSION) {
83
+ const invalidDb = db;
84
+ db = undefined;
85
+ invalidDb.close();
86
+ return handleUnavailableIndex(indexPath, options);
87
+ }
88
+
89
+ return { db, status };
90
+ } catch (error) {
91
+ db?.close();
92
+ return handleUnavailableIndex(indexPath, options, error);
93
+ }
94
+ }
95
+
96
+ export function getIndexStatusFromDb(dbPath: string, db: SessionIndexDatabase): SessionIndexStatus {
97
+ const schemaVersionRaw = getMetadata(db, "schema_version");
98
+ const lastFullReindexAt = getMetadata(db, "indexed_at");
99
+ const sessionCountRow = parseTypeBoxValue(
100
+ ROW_COUNT_SCHEMA,
101
+ db.prepare(`SELECT COUNT(*) as count FROM sessions`).get(),
102
+ "Invalid session count row",
103
+ );
104
+
105
+ return {
106
+ dbPath,
107
+ exists: true,
108
+ schemaVersion: schemaVersionRaw ? Number(schemaVersionRaw) : undefined,
109
+ sessionCount: sessionCountRow.count,
110
+ lastFullReindexAt,
111
+ };
112
+ }
113
+
114
+ export function formatRequiredSessionIndexError(indexPath: string): string {
115
+ return `Session index missing or incompatible at ${indexPath}. Run /session-index and press r to rebuild it.`;
116
+ }
117
+
118
+ function handleUnavailableIndex(
119
+ indexPath: string,
120
+ options: WithSessionIndexOptions,
121
+ cause?: unknown,
122
+ ): undefined {
123
+ if (!options.required) {
124
+ return undefined;
125
+ }
126
+
127
+ throw new Error(formatRequiredSessionIndexError(indexPath), { cause });
128
+ }