pi-sessions 0.6.0 → 0.7.1
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/README.md +22 -11
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +79 -131
- package/extensions/session-auto-title/model.ts +3 -11
- package/extensions/session-handoff/picker.ts +50 -4
- package/extensions/session-handoff/query.ts +74 -56
- package/extensions/session-handoff/refs.ts +12 -18
- package/extensions/session-handoff.ts +36 -27
- package/extensions/session-messaging/broker/process.ts +23 -26
- package/extensions/session-messaging/broker/spawn.ts +24 -3
- package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
- package/extensions/session-messaging/pi/message-contracts.ts +9 -10
- package/extensions/session-messaging/pi/message-view.ts +12 -1
- package/extensions/session-messaging/pi/service.ts +117 -62
- package/extensions/session-messaging/pi/tools.ts +4 -56
- package/extensions/session-search/extract.ts +86 -436
- package/extensions/session-search/hooks.ts +17 -25
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +1 -0
- package/extensions/session-search.ts +157 -128
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
- package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
- package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +43 -48
- package/extensions/shared/session-index/index.ts +1 -0
- package/extensions/shared/session-index/lineage.ts +10 -0
- package/extensions/shared/session-index/query/ast.ts +40 -0
- package/extensions/shared/session-index/query/compiler.ts +146 -0
- package/extensions/shared/session-index/query/lexer.ts +140 -0
- package/extensions/shared/session-index/query/parser.ts +178 -0
- package/extensions/shared/session-index/schema.ts +22 -6
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +552 -278
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +12 -21
- package/extensions/shared/settings.ts +61 -4
- package/extensions/shared/text.ts +50 -0
- package/package.json +5 -4
- /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import net from "node:net";
|
|
4
|
-
import { readFrames, writeFrame } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type SessionMessagingSessionInfo,
|
|
9
|
-
} from "../shared/protocol.ts";
|
|
10
|
-
import { getSessionMessagingSocketPath } from "../shared/socket-path.ts";
|
|
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
|
+
|
|
11
8
|
export const INCOMING_SESSION_MESSAGE_EVENT = "session_messaging.incoming_message";
|
|
12
9
|
|
|
13
10
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
14
11
|
const SOCKET_PATH = getSessionMessagingSocketPath();
|
|
15
12
|
|
|
16
13
|
interface PendingListRequest {
|
|
17
|
-
resolve(
|
|
14
|
+
resolve(sessionIds: string[]): void;
|
|
18
15
|
reject(error: Error): void;
|
|
19
16
|
timeout: NodeJS.Timeout;
|
|
20
17
|
}
|
|
@@ -42,19 +39,19 @@ export interface SendSessionMessageOptions {
|
|
|
42
39
|
|
|
43
40
|
export class SessionMessagingClient extends EventEmitter {
|
|
44
41
|
private socket: net.Socket | undefined;
|
|
45
|
-
private
|
|
42
|
+
private sessionId: string | undefined;
|
|
46
43
|
private pendingLists = new Map<string, PendingListRequest>();
|
|
47
44
|
private pendingSends = new Map<string, PendingSendRequest>();
|
|
48
45
|
|
|
49
46
|
get isConnected(): boolean {
|
|
50
|
-
return Boolean(this.socket && !this.socket.destroyed && this.socket.writable && this.
|
|
47
|
+
return Boolean(this.socket && !this.socket.destroyed && this.socket.writable && this.sessionId);
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
get
|
|
54
|
-
return this.
|
|
50
|
+
get registeredSessionId(): string | undefined {
|
|
51
|
+
return this.sessionId;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
async connect(
|
|
54
|
+
async connect(sessionId: string): Promise<void> {
|
|
58
55
|
if (this.isConnected) {
|
|
59
56
|
return;
|
|
60
57
|
}
|
|
@@ -112,7 +109,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
112
109
|
void this.readSocket(socket);
|
|
113
110
|
|
|
114
111
|
socket.setKeepAlive(true, 5_000);
|
|
115
|
-
writeFrame(socket, { type: "register",
|
|
112
|
+
writeFrame(socket, { type: "register", sessionId });
|
|
116
113
|
});
|
|
117
114
|
}
|
|
118
115
|
|
|
@@ -128,7 +125,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
128
125
|
this.cleanup();
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
|
|
128
|
+
listSessionIds(): Promise<string[]> {
|
|
132
129
|
const socket = this.requireSocket();
|
|
133
130
|
const requestId = randomUUID();
|
|
134
131
|
|
|
@@ -197,7 +194,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
197
194
|
private handleFrame(frame: SessionMessagingBrokerFrame): void {
|
|
198
195
|
switch (frame.type) {
|
|
199
196
|
case "registered":
|
|
200
|
-
this.
|
|
197
|
+
this.sessionId = frame.sessionId;
|
|
201
198
|
this.emit("registered");
|
|
202
199
|
break;
|
|
203
200
|
case "register_failed":
|
|
@@ -208,7 +205,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
208
205
|
if (!pending) return;
|
|
209
206
|
clearTimeout(pending.timeout);
|
|
210
207
|
this.pendingLists.delete(frame.requestId);
|
|
211
|
-
pending.resolve(frame.
|
|
208
|
+
pending.resolve(frame.sessionIds);
|
|
212
209
|
break;
|
|
213
210
|
}
|
|
214
211
|
case "incoming":
|
|
@@ -233,7 +230,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
233
230
|
}
|
|
234
231
|
|
|
235
232
|
private requireSocket(): net.Socket {
|
|
236
|
-
if (!this.socket || this.socket.destroyed || !this.socket.writable || !this.
|
|
233
|
+
if (!this.socket || this.socket.destroyed || !this.socket.writable || !this.sessionId) {
|
|
237
234
|
throw new Error("Session messaging is not connected.");
|
|
238
235
|
}
|
|
239
236
|
|
|
@@ -241,7 +238,7 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
241
238
|
}
|
|
242
239
|
|
|
243
240
|
private cleanup(): void {
|
|
244
|
-
this.
|
|
241
|
+
this.sessionId = undefined;
|
|
245
242
|
for (const pending of this.pendingLists.values()) {
|
|
246
243
|
clearTimeout(pending.timeout);
|
|
247
244
|
pending.reject(new Error("Session messaging disconnected."));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Socket } from "node:net";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { Static, TSchema } from "typebox";
|
|
4
|
-
import { parseTypeBoxValue } from "
|
|
4
|
+
import { parseTypeBoxValue } from "../typebox.ts";
|
|
5
5
|
|
|
6
6
|
const MAX_FRAME_BYTES = 256 * 1024;
|
|
7
7
|
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { type Static, Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
const SESSION_INFO_SCHEMA = Type.Object({
|
|
4
|
-
sessionId: Type.String(),
|
|
5
|
-
sessionName: Type.Optional(Type.String()),
|
|
6
|
-
cwd: Type.String(),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
3
|
export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
|
|
10
4
|
messageId: Type.String(),
|
|
11
|
-
source:
|
|
12
|
-
target:
|
|
5
|
+
source: Type.String(),
|
|
6
|
+
target: Type.String(),
|
|
13
7
|
body: Type.String(),
|
|
14
8
|
requestResponse: Type.Optional(Type.Boolean()),
|
|
15
9
|
sentAt: Type.String(),
|
|
@@ -18,7 +12,7 @@ export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
|
|
|
18
12
|
|
|
19
13
|
const REGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
20
14
|
type: Type.Literal("register"),
|
|
21
|
-
|
|
15
|
+
sessionId: Type.String(),
|
|
22
16
|
});
|
|
23
17
|
const UNREGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
24
18
|
type: Type.Literal("unregister"),
|
|
@@ -47,7 +41,7 @@ const INCOMING_ACK_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
|
47
41
|
|
|
48
42
|
const REGISTERED_BROKER_FRAME_SCHEMA = Type.Object({
|
|
49
43
|
type: Type.Literal("registered"),
|
|
50
|
-
|
|
44
|
+
sessionId: Type.String(),
|
|
51
45
|
});
|
|
52
46
|
const REGISTER_FAILED_BROKER_FRAME_SCHEMA = Type.Object({
|
|
53
47
|
type: Type.Literal("register_failed"),
|
|
@@ -56,7 +50,7 @@ const REGISTER_FAILED_BROKER_FRAME_SCHEMA = Type.Object({
|
|
|
56
50
|
const SESSIONS_BROKER_FRAME_SCHEMA = Type.Object({
|
|
57
51
|
type: Type.Literal("sessions"),
|
|
58
52
|
requestId: Type.String(),
|
|
59
|
-
|
|
53
|
+
sessionIds: Type.Array(Type.String()),
|
|
60
54
|
});
|
|
61
55
|
const INCOMING_BROKER_FRAME_SCHEMA = Type.Object({
|
|
62
56
|
type: Type.Literal("incoming"),
|
|
@@ -92,7 +86,6 @@ export const BROKER_FRAME_SCHEMA = Type.Union([
|
|
|
92
86
|
ERROR_BROKER_FRAME_SCHEMA,
|
|
93
87
|
]);
|
|
94
88
|
|
|
95
|
-
export type SessionMessagingSessionInfo = Static<typeof SESSION_INFO_SCHEMA>;
|
|
96
89
|
export type SessionMessagePayload = Static<typeof SESSION_MESSAGE_PAYLOAD_SCHEMA>;
|
|
97
90
|
export type SessionMessagingSendClientFrame = Static<typeof SEND_CLIENT_FRAME_SCHEMA>;
|
|
98
91
|
export type SessionMessagingIncomingAckClientFrame = Static<
|
|
@@ -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
|
+
}
|
|
@@ -3,10 +3,11 @@ import type { FileTouchOp, FileTouchSource, PathScope } from "../../session-sear
|
|
|
3
3
|
import { safeParseTypeBoxJson } from "../typebox.ts";
|
|
4
4
|
import type { SqliteDatabase } from "./sqlite.ts";
|
|
5
5
|
|
|
6
|
-
export const INDEX_SCHEMA_VERSION =
|
|
6
|
+
export const INDEX_SCHEMA_VERSION = 12;
|
|
7
7
|
|
|
8
8
|
export type SessionOrigin = "handoff" | "fork" | "unknown_child";
|
|
9
9
|
export type SessionLineageRelation =
|
|
10
|
+
| "self"
|
|
10
11
|
| "parent"
|
|
11
12
|
| "ancestor"
|
|
12
13
|
| "child"
|
|
@@ -14,6 +15,8 @@ export type SessionLineageRelation =
|
|
|
14
15
|
| "sibling"
|
|
15
16
|
| "ancestor_sibling";
|
|
16
17
|
|
|
18
|
+
export type SearchSort = "relevance" | "modified_desc" | "modified_asc";
|
|
19
|
+
|
|
17
20
|
export interface SessionRow {
|
|
18
21
|
sessionId: string;
|
|
19
22
|
sessionPath: string;
|
|
@@ -58,7 +61,7 @@ export interface SessionRelatedSessionRow extends SessionLineageRow {
|
|
|
58
61
|
export interface SessionTextChunkRow {
|
|
59
62
|
id?: number | undefined;
|
|
60
63
|
sessionId: string;
|
|
61
|
-
entryId
|
|
64
|
+
entryId: string;
|
|
62
65
|
entryType: string;
|
|
63
66
|
role?: string | undefined;
|
|
64
67
|
ts: string;
|
|
@@ -89,9 +92,34 @@ export interface SearchSessionsParams {
|
|
|
89
92
|
after?: string | undefined;
|
|
90
93
|
before?: string | undefined;
|
|
91
94
|
touched?: string[] | undefined;
|
|
95
|
+
changed?: string[] | undefined;
|
|
96
|
+
sort?: SearchSort | undefined;
|
|
92
97
|
limit?: number | undefined;
|
|
93
98
|
excludeSessionIds?: string[] | undefined;
|
|
94
|
-
|
|
99
|
+
includeSessionIds?: string[] | undefined;
|
|
100
|
+
relativeToSessionId?: string | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type SessionSearchEvidence =
|
|
104
|
+
| {
|
|
105
|
+
kind: "text";
|
|
106
|
+
sourceKind: string;
|
|
107
|
+
snippet: string;
|
|
108
|
+
score: number;
|
|
109
|
+
entryId: string;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
kind: "file_touch";
|
|
113
|
+
op: "read" | "changed";
|
|
114
|
+
query: string;
|
|
115
|
+
path: string;
|
|
116
|
+
entryId?: string | undefined;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
kind: "session_id";
|
|
120
|
+
match: "exact" | "prefix" | "substring";
|
|
121
|
+
score: number;
|
|
122
|
+
};
|
|
95
123
|
|
|
96
124
|
export interface SearchSessionResult {
|
|
97
125
|
sessionId: string;
|
|
@@ -108,8 +136,9 @@ export interface SearchSessionResult {
|
|
|
108
136
|
sessionOrigin?: SessionOrigin | undefined;
|
|
109
137
|
handoffGoal?: string | undefined;
|
|
110
138
|
handoffNextTask?: string | undefined;
|
|
139
|
+
relation?: SessionLineageRelation | undefined;
|
|
111
140
|
snippet: string;
|
|
112
|
-
|
|
141
|
+
evidence: SessionSearchEvidence[];
|
|
113
142
|
score: number;
|
|
114
143
|
hitCount: number;
|
|
115
144
|
}
|
|
@@ -131,6 +160,7 @@ export const SESSION_ORIGIN_SCHEMA = Type.Union([
|
|
|
131
160
|
Type.Literal("unknown_child"),
|
|
132
161
|
]);
|
|
133
162
|
export const SESSION_LINEAGE_RELATION_SCHEMA = Type.Union([
|
|
163
|
+
Type.Literal("self"),
|
|
134
164
|
Type.Literal("parent"),
|
|
135
165
|
Type.Literal("ancestor"),
|
|
136
166
|
Type.Literal("child"),
|
|
@@ -150,7 +180,7 @@ export function parseRepoRoots(value: string): string[] {
|
|
|
150
180
|
}
|
|
151
181
|
|
|
152
182
|
export function escapeLikePrefix(value: string): string {
|
|
153
|
-
return value.replace(/[%_]/g, "\\$&");
|
|
183
|
+
return value.replace(/[%_\\]/g, "\\$&");
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
export function normalizeTimeFilter(value?: string): string | undefined {
|
|
@@ -160,7 +190,7 @@ export function normalizeTimeFilter(value?: string): string | undefined {
|
|
|
160
190
|
|
|
161
191
|
const date = new Date(value);
|
|
162
192
|
if (Number.isNaN(date.getTime())) {
|
|
163
|
-
|
|
193
|
+
throw new Error(`Invalid time filter value: ${value}`);
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
return date.toISOString();
|
|
@@ -174,48 +204,6 @@ export function sanitizeFilterValues(values?: string[]): string[] {
|
|
|
174
204
|
return values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
175
205
|
}
|
|
176
206
|
|
|
177
|
-
export function boostIndependentHits(hitCount: number): number {
|
|
178
|
-
return hitCount > 1 ? (hitCount - 1) * 0.75 : 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function buildFtsQuery(query: string): string | undefined {
|
|
182
|
-
const trimmed = query.trim();
|
|
183
|
-
if (!trimmed) {
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
188
|
-
const exact = sanitizeFtsToken(trimmed.slice(1, -1));
|
|
189
|
-
return exact ? quoteFtsToken(exact) : undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const tokens = tokenizeSearchTerms(trimmed);
|
|
193
|
-
if (tokens.length === 0) {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return tokens.map(quoteFtsPrefixToken).join(" AND ");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function tokenizeSearchTerms(query: string): string[] {
|
|
201
|
-
return query
|
|
202
|
-
.split(/\s+/)
|
|
203
|
-
.flatMap((token) => sanitizeFtsToken(token).split(/\s+/))
|
|
204
|
-
.filter((token) => token.length > 0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function sanitizeFtsToken(token: string): string {
|
|
208
|
-
return token.replace(/[^A-Za-z0-9_]+/g, " ").trim();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function quoteFtsToken(token: string): string {
|
|
212
|
-
return `"${token.replace(/"/g, '""')}"`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function quoteFtsPrefixToken(token: string): string {
|
|
216
|
-
return `${quoteFtsToken(token)}*`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
207
|
export function compactSearchValue(value: string): string {
|
|
220
208
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
221
209
|
}
|
|
@@ -223,3 +211,10 @@ export function compactSearchValue(value: string): string {
|
|
|
223
211
|
export function compactSessionId(value: string): string {
|
|
224
212
|
return value.toLowerCase().replace(/-/g, "");
|
|
225
213
|
}
|
|
214
|
+
|
|
215
|
+
export function tokenizeSearchText(value: string): string[] {
|
|
216
|
+
return value
|
|
217
|
+
.split(/[^A-Za-z0-9_]+/g)
|
|
218
|
+
.map((token) => token.trim())
|
|
219
|
+
.filter((token) => token.length > 0);
|
|
220
|
+
}
|
|
@@ -346,6 +346,7 @@ function queryRelatedSessions(
|
|
|
346
346
|
WHERE r.session_id = ?${relationFilter}
|
|
347
347
|
ORDER BY
|
|
348
348
|
CASE r.relation
|
|
349
|
+
WHEN 'self' THEN 0
|
|
349
350
|
WHEN 'parent' THEN 1
|
|
350
351
|
WHEN 'child' THEN 2
|
|
351
352
|
WHEN 'sibling' THEN 3
|
|
@@ -371,6 +372,13 @@ function collectMaterializedLineageRows(
|
|
|
371
372
|
childrenByParent: Map<string, string[]>,
|
|
372
373
|
): Map<string, MaterializedLineageRow> {
|
|
373
374
|
const relations = new Map<string, MaterializedLineageRow>();
|
|
375
|
+
setMaterializedLineageRow(relations, {
|
|
376
|
+
sessionId,
|
|
377
|
+
relatedSessionId: sessionId,
|
|
378
|
+
relation: "self",
|
|
379
|
+
distance: 0,
|
|
380
|
+
});
|
|
381
|
+
|
|
374
382
|
const visitedAncestors = new Set<string>();
|
|
375
383
|
const ancestors: Array<{ sessionId: string; distance: number }> = [];
|
|
376
384
|
|
|
@@ -476,6 +484,8 @@ function setMaterializedLineageRow(
|
|
|
476
484
|
|
|
477
485
|
function getLineageRelationPriority(relation: SessionLineageRelation): number {
|
|
478
486
|
switch (relation) {
|
|
487
|
+
case "self":
|
|
488
|
+
return 0;
|
|
479
489
|
case "parent":
|
|
480
490
|
return 1;
|
|
481
491
|
case "child":
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface QueryTermNode {
|
|
2
|
+
kind: "term";
|
|
3
|
+
value: string;
|
|
4
|
+
quoted: boolean;
|
|
5
|
+
prefix: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface QueryAndNode {
|
|
9
|
+
kind: "and";
|
|
10
|
+
children: SearchQueryNode[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueryOrNode {
|
|
14
|
+
kind: "or";
|
|
15
|
+
children: SearchQueryNode[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryAdjacencyNode {
|
|
19
|
+
kind: "adjacency";
|
|
20
|
+
children: QueryTermNode[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface QueryNotNode {
|
|
24
|
+
kind: "not";
|
|
25
|
+
child: SearchQueryNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SearchQueryNode =
|
|
29
|
+
| QueryTermNode
|
|
30
|
+
| QueryAndNode
|
|
31
|
+
| QueryOrNode
|
|
32
|
+
| QueryAdjacencyNode
|
|
33
|
+
| QueryNotNode;
|
|
34
|
+
|
|
35
|
+
export class SearchQuerySyntaxError extends Error {
|
|
36
|
+
constructor(message: string) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "SearchQuerySyntaxError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type SearchQueryNode, SearchQuerySyntaxError } from "./ast.ts";
|
|
2
|
+
import { parseSearchQuery } from "./parser.ts";
|
|
3
|
+
|
|
4
|
+
export interface CompiledSearchQuery {
|
|
5
|
+
match: string;
|
|
6
|
+
relaxedMatch?: string | undefined;
|
|
7
|
+
excludes: string[];
|
|
8
|
+
positiveTerms: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function compileSearchQuery(input: string): CompiledSearchQuery | undefined {
|
|
12
|
+
const ast = parseSearchQuery(input);
|
|
13
|
+
if (!ast) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { positive, excludes } = splitTopLevelExcludes(ast);
|
|
18
|
+
if (!positive) {
|
|
19
|
+
throw new SearchQuerySyntaxError(
|
|
20
|
+
"Negative-only searches are not supported yet. Add at least one positive search term.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
validateNoNestedNegation(positive);
|
|
25
|
+
for (const exclude of excludes) {
|
|
26
|
+
validateNoNestedNegation(exclude);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const match = compilePositiveNode(positive);
|
|
30
|
+
const relaxedMatch = compilePositiveNode(positive, "OR");
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
match,
|
|
34
|
+
relaxedMatch: relaxedMatch === match ? undefined : relaxedMatch,
|
|
35
|
+
excludes: excludes.map((exclude) => compilePositiveNode(exclude)),
|
|
36
|
+
positiveTerms: collectPositiveTerms(positive),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SplitQuery {
|
|
41
|
+
positive: SearchQueryNode | undefined;
|
|
42
|
+
excludes: SearchQueryNode[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitTopLevelExcludes(node: SearchQueryNode): SplitQuery {
|
|
46
|
+
if (node.kind === "not") {
|
|
47
|
+
return { positive: undefined, excludes: [node.child] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (node.kind !== "and") {
|
|
51
|
+
return { positive: node, excludes: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const positives: SearchQueryNode[] = [];
|
|
55
|
+
const excludes: SearchQueryNode[] = [];
|
|
56
|
+
|
|
57
|
+
for (const child of flattenAndChildren(node)) {
|
|
58
|
+
if (child.kind === "not") {
|
|
59
|
+
excludes.push(child.child);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
positives.push(child);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
positive: buildAndNode(positives),
|
|
68
|
+
excludes,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flattenAndChildren(node: SearchQueryNode): SearchQueryNode[] {
|
|
73
|
+
if (node.kind !== "and") {
|
|
74
|
+
return [node];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return node.children.flatMap(flattenAndChildren);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildAndNode(children: SearchQueryNode[]): SearchQueryNode | undefined {
|
|
81
|
+
if (children.length === 0) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (children.length === 1) {
|
|
86
|
+
return children[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { kind: "and", children };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateNoNestedNegation(node: SearchQueryNode): void {
|
|
93
|
+
switch (node.kind) {
|
|
94
|
+
case "not":
|
|
95
|
+
throw new SearchQuerySyntaxError(
|
|
96
|
+
"Negation is only supported as a top-level AND clause in this release.",
|
|
97
|
+
);
|
|
98
|
+
case "and":
|
|
99
|
+
case "or":
|
|
100
|
+
case "adjacency":
|
|
101
|
+
for (const child of node.children) {
|
|
102
|
+
validateNoNestedNegation(child);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
case "term":
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function compilePositiveNode(node: SearchQueryNode, adjacencyOp: "AND" | "OR" = "AND"): string {
|
|
111
|
+
switch (node.kind) {
|
|
112
|
+
case "term":
|
|
113
|
+
return compileTerm(node.value, node.prefix);
|
|
114
|
+
case "and":
|
|
115
|
+
return `(${node.children.map((child) => compilePositiveNode(child, adjacencyOp)).join(" AND ")})`;
|
|
116
|
+
case "or":
|
|
117
|
+
return `(${node.children.map((child) => compilePositiveNode(child, adjacencyOp)).join(" OR ")})`;
|
|
118
|
+
case "adjacency":
|
|
119
|
+
return `(${node.children.map((child) => compileTerm(child.value, child.prefix)).join(` ${adjacencyOp} `)})`;
|
|
120
|
+
case "not":
|
|
121
|
+
throw new SearchQuerySyntaxError(
|
|
122
|
+
"Negation is only supported as a top-level AND clause in this release.",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compileTerm(value: string, prefix: boolean): string {
|
|
128
|
+
return `${quoteFtsValue(value)}${prefix ? "*" : ""}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function quoteFtsValue(value: string): string {
|
|
132
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function collectPositiveTerms(node: SearchQueryNode): string[] {
|
|
136
|
+
switch (node.kind) {
|
|
137
|
+
case "term":
|
|
138
|
+
return [node.value];
|
|
139
|
+
case "and":
|
|
140
|
+
case "or":
|
|
141
|
+
case "adjacency":
|
|
142
|
+
return node.children.flatMap(collectPositiveTerms);
|
|
143
|
+
case "not":
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|