@chendpoc/pi-memory 0.1.0 → 0.1.12
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 +156 -111
- package/dist/adapters/ollamaClient.d.ts +11 -0
- package/dist/adapters/ollamaClient.d.ts.map +1 -0
- package/dist/adapters/ollamaClient.js +122 -0
- package/dist/adapters/ollamaClient.js.map +1 -0
- package/dist/adapters/openaiCompatClient.d.ts +11 -0
- package/dist/adapters/openaiCompatClient.d.ts.map +1 -0
- package/dist/adapters/openaiCompatClient.js +118 -0
- package/dist/adapters/openaiCompatClient.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/fallback/sessionIndex.d.ts.map +1 -1
- package/dist/fallback/sessionIndex.js +90 -25
- package/dist/fallback/sessionIndex.js.map +1 -1
- package/dist/fallback/sessionSearch.d.ts +1 -1
- package/dist/fallback/sessionSearch.d.ts.map +1 -1
- package/dist/fallback/sessionSearch.js +101 -28
- package/dist/fallback/sessionSearch.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/local/graphQuery.d.ts +21 -0
- package/dist/local/graphQuery.d.ts.map +1 -0
- package/dist/local/graphQuery.js +170 -0
- package/dist/local/graphQuery.js.map +1 -0
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +57 -17
- package/dist/pi-extension.js.map +1 -1
- package/dist/service.d.ts +10 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +72 -30
- package/dist/service.js.map +1 -1
- package/dist/settings.d.ts +38 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +68 -0
- package/dist/settings.js.map +1 -0
- package/dist/sidecar/process.d.ts.map +1 -1
- package/dist/sidecar/process.js +16 -4
- package/dist/sidecar/process.js.map +1 -1
- package/dist/trainer/sessionLoader.d.ts +2 -2
- package/dist/trainer/sessionLoader.d.ts.map +1 -1
- package/dist/trainer/sessionLoader.js +115 -39
- package/dist/trainer/sessionLoader.js.map +1 -1
- package/package.json +8 -4
- package/src/adapters/ollamaClient.ts +179 -0
- package/src/adapters/openaiCompatClient.ts +155 -0
- package/src/cache/memoryCaches.ts +72 -0
- package/src/cli.ts +4 -3
- package/src/fallback/llmRerank.ts +8 -1
- package/src/fallback/sessionIndex.ts +78 -40
- package/src/fallback/sessionSearch.ts +107 -27
- package/src/index.ts +28 -0
- package/src/local/graphQuery.ts +252 -0
- package/src/paths.ts +1 -1
- package/src/pi-extension.ts +164 -36
- package/src/preflight/detectIntents.ts +6 -0
- package/src/preflight/hook.ts +68 -5
- package/src/preflight/render.ts +28 -3
- package/src/service.ts +133 -29
- package/src/settings.ts +126 -0
- package/src/sidecar/process.ts +19 -4
- package/src/tools/memoryRecall.ts +33 -9
- package/src/trainer/scheduler.ts +3 -0
- package/src/trainer/sessionLoader.ts +128 -42
|
@@ -16,10 +16,6 @@ export interface SessionSearchHit {
|
|
|
16
16
|
let cachedIndex: SessionIndex | null = null;
|
|
17
17
|
let cachedDbPath: string | null = null;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Get or open the FTS5 session index. Returns null if better-sqlite3
|
|
21
|
-
* is unavailable or the DB file doesn't exist.
|
|
22
|
-
*/
|
|
23
19
|
function getSessionIndex(dbPath: string): SessionIndex | null {
|
|
24
20
|
if (cachedIndex && cachedDbPath === dbPath) return cachedIndex;
|
|
25
21
|
if (!fsSync.existsSync(dbPath)) return null;
|
|
@@ -47,8 +43,29 @@ interface PiSessionFile {
|
|
|
47
43
|
|
|
48
44
|
const SNIPPET_MAX = 240;
|
|
49
45
|
|
|
46
|
+
async function collectFiles(dir: string): Promise<string[]> {
|
|
47
|
+
let names: string[];
|
|
48
|
+
try {
|
|
49
|
+
names = await fs.readdir(dir);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const files: string[] = [];
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
const full = path.join(dir, name);
|
|
56
|
+
let st;
|
|
57
|
+
try { st = await fs.stat(full); } catch { continue; }
|
|
58
|
+
if (st.isDirectory()) {
|
|
59
|
+
files.push(...await collectFiles(full));
|
|
60
|
+
} else if (st.isFile() && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
61
|
+
files.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/**
|
|
51
|
-
* Keyword search over Pi-style session
|
|
68
|
+
* Keyword search over Pi-style session files (JSON + JSONL, recursive subdirectories).
|
|
52
69
|
* Uses FTS5 index when available, falls back to file scan.
|
|
53
70
|
* All whitespace-separated terms must match (case-insensitive AND).
|
|
54
71
|
*/
|
|
@@ -72,17 +89,10 @@ export async function sessionKeywordSearch(
|
|
|
72
89
|
const terms = splitTerms(q);
|
|
73
90
|
if (terms.length === 0) return [];
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
entries = await fs.readdir(sessionsDir);
|
|
78
|
-
} catch {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
|
|
92
|
+
const filePaths = await collectFiles(sessionsDir);
|
|
82
93
|
const hits: SessionSearchHit[] = [];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const filePath = path.join(sessionsDir, name);
|
|
94
|
+
|
|
95
|
+
for (const filePath of filePaths) {
|
|
86
96
|
let st;
|
|
87
97
|
try {
|
|
88
98
|
st = await fs.stat(filePath);
|
|
@@ -91,34 +101,104 @@ export async function sessionKeywordSearch(
|
|
|
91
101
|
}
|
|
92
102
|
if (!st.isFile()) continue;
|
|
93
103
|
|
|
94
|
-
let
|
|
104
|
+
let raw: string;
|
|
105
|
+
try {
|
|
106
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (filePath.endsWith(".jsonl")) {
|
|
112
|
+
scanJsonlFile(raw, filePath, terms, hits, limit);
|
|
113
|
+
} else {
|
|
114
|
+
scanJsonFile(raw, filePath, terms, hits, limit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hits.length >= limit) return hits;
|
|
118
|
+
}
|
|
119
|
+
return hits;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scanJsonFile(
|
|
123
|
+
raw: string,
|
|
124
|
+
filePath: string,
|
|
125
|
+
terms: string[],
|
|
126
|
+
hits: SessionSearchHit[],
|
|
127
|
+
limit: number,
|
|
128
|
+
): void {
|
|
129
|
+
let session: PiSessionFile;
|
|
130
|
+
try {
|
|
131
|
+
session = JSON.parse(raw) as PiSessionFile;
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionId = session.id ?? path.basename(filePath, ".json");
|
|
137
|
+
const title = session.title ?? "";
|
|
138
|
+
const createdAt = session.created_at ?? "";
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < (session.messages?.length ?? 0); i++) {
|
|
141
|
+
const msg = session.messages![i]!;
|
|
142
|
+
const text = messageText(msg.content);
|
|
143
|
+
if (!text || !allTermsMatch(text, terms)) continue;
|
|
144
|
+
hits.push({
|
|
145
|
+
session_id: sessionId,
|
|
146
|
+
session_title: title,
|
|
147
|
+
role: msg.role ?? "unknown",
|
|
148
|
+
snippet: makeSnippet(text, terms[0]!),
|
|
149
|
+
msg_index: i,
|
|
150
|
+
created_at: createdAt,
|
|
151
|
+
});
|
|
152
|
+
if (hits.length >= limit) return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scanJsonlFile(
|
|
157
|
+
raw: string,
|
|
158
|
+
filePath: string,
|
|
159
|
+
terms: string[],
|
|
160
|
+
hits: SessionSearchHit[],
|
|
161
|
+
limit: number,
|
|
162
|
+
): void {
|
|
163
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
164
|
+
let sessionId = path.basename(filePath, ".jsonl");
|
|
165
|
+
let title = "";
|
|
166
|
+
let createdAt = "";
|
|
167
|
+
let msgIndex = 0;
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
let obj: Record<string, unknown>;
|
|
95
171
|
try {
|
|
96
|
-
|
|
97
|
-
session = JSON.parse(raw) as PiSessionFile;
|
|
172
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
98
173
|
} catch {
|
|
99
174
|
continue;
|
|
100
175
|
}
|
|
101
176
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
177
|
+
if (obj.type === "session") {
|
|
178
|
+
sessionId = (obj.id as string) ?? sessionId;
|
|
179
|
+
title = (obj.title as string) ?? "";
|
|
180
|
+
createdAt = (obj.timestamp as string) ?? "";
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
105
183
|
|
|
106
|
-
|
|
107
|
-
const msg =
|
|
184
|
+
if (obj.type === "message") {
|
|
185
|
+
const msg = (obj as { message?: PiSessionMessage }).message;
|
|
186
|
+
if (!msg?.role || !msg.content) continue;
|
|
187
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
108
188
|
const text = messageText(msg.content);
|
|
109
189
|
if (!text || !allTermsMatch(text, terms)) continue;
|
|
110
190
|
hits.push({
|
|
111
191
|
session_id: sessionId,
|
|
112
192
|
session_title: title,
|
|
113
|
-
role: msg.role
|
|
193
|
+
role: msg.role,
|
|
114
194
|
snippet: makeSnippet(text, terms[0]!),
|
|
115
|
-
msg_index:
|
|
195
|
+
msg_index: msgIndex,
|
|
116
196
|
created_at: createdAt,
|
|
117
197
|
});
|
|
118
|
-
|
|
198
|
+
msgIndex++;
|
|
199
|
+
if (hits.length >= limit) return;
|
|
119
200
|
}
|
|
120
201
|
}
|
|
121
|
-
return hits;
|
|
122
202
|
}
|
|
123
203
|
|
|
124
204
|
function splitTerms(query: string): string[] {
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,15 @@ export {
|
|
|
25
25
|
type MemoryProvider,
|
|
26
26
|
} from "./config.js";
|
|
27
27
|
|
|
28
|
+
export {
|
|
29
|
+
defaultMemoryConfigPath,
|
|
30
|
+
loadMemoryConfig,
|
|
31
|
+
loadMemorySettings,
|
|
32
|
+
resolveHelperModelSpec,
|
|
33
|
+
type LoadedMemorySettings,
|
|
34
|
+
type MemorySettingsFile,
|
|
35
|
+
} from "./settings.js";
|
|
36
|
+
|
|
28
37
|
export {
|
|
29
38
|
defaultBundleRoot,
|
|
30
39
|
defaultPiHome,
|
|
@@ -88,10 +97,12 @@ export {
|
|
|
88
97
|
|
|
89
98
|
export {
|
|
90
99
|
PRIVATE_MEMORY_BODY_BYTE_CAP,
|
|
100
|
+
SEMANTIC_FALLBACK_CANDIDATES,
|
|
91
101
|
renderFallbackPrivateMemory,
|
|
92
102
|
renderPrivateMemoryContext,
|
|
93
103
|
sanitizeUserBlock,
|
|
94
104
|
truncatePrivateMemoryBody,
|
|
105
|
+
type FallbackRenderOptions,
|
|
95
106
|
type PreflightQueryResult,
|
|
96
107
|
} from "./preflight/render.js";
|
|
97
108
|
|
|
@@ -206,6 +217,23 @@ export {
|
|
|
206
217
|
|
|
207
218
|
export { defaultSessionDbPath } from "./fallback/sessionSearch.js";
|
|
208
219
|
|
|
220
|
+
export { LocalGraphQuerier } from "./local/graphQuery.js";
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
createOllamaLLMClient,
|
|
224
|
+
createOllamaMemoryHelper,
|
|
225
|
+
ollamaHealthCheck,
|
|
226
|
+
DEFAULT_OLLAMA_CONFIG,
|
|
227
|
+
type OllamaConfig,
|
|
228
|
+
} from "./adapters/ollamaClient.js";
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
createOpenAICompatLLMClient,
|
|
232
|
+
createOpenAICompatMemoryHelper,
|
|
233
|
+
openaiCompatHealthCheck,
|
|
234
|
+
type OpenAICompatConfig,
|
|
235
|
+
} from "./adapters/openaiCompatClient.js";
|
|
236
|
+
|
|
209
237
|
export {
|
|
210
238
|
rerankWithLLM,
|
|
211
239
|
type RerankOptions,
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ErrorClass,
|
|
6
|
+
MemoryBlock,
|
|
7
|
+
MemoryCandidateGroup,
|
|
8
|
+
QueryCandidate,
|
|
9
|
+
QueryIntent,
|
|
10
|
+
ResponseEnvelope,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
|
|
13
|
+
interface BundleEntity {
|
|
14
|
+
entity_id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
type: string;
|
|
17
|
+
aliases: string[];
|
|
18
|
+
mention_count: number;
|
|
19
|
+
distinct_session_count: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BundleEdge {
|
|
23
|
+
head_entity_id: string;
|
|
24
|
+
relation: string;
|
|
25
|
+
tail_entity_id: string;
|
|
26
|
+
supporting_event_ids: string[];
|
|
27
|
+
evidence: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BundleEvent {
|
|
31
|
+
event_id: string;
|
|
32
|
+
description: string;
|
|
33
|
+
session_id: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface GraphData {
|
|
38
|
+
entities: BundleEntity[];
|
|
39
|
+
edges: BundleEdge[];
|
|
40
|
+
events: BundleEvent[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class LocalGraphQuerier {
|
|
44
|
+
private entities: BundleEntity[] = [];
|
|
45
|
+
private edges: BundleEdge[] = [];
|
|
46
|
+
private events: BundleEvent[] = [];
|
|
47
|
+
private entityById = new Map<string, BundleEntity>();
|
|
48
|
+
private entityByLabel = new Map<string, BundleEntity>();
|
|
49
|
+
private loaded = false;
|
|
50
|
+
private graphMtime = 0;
|
|
51
|
+
|
|
52
|
+
constructor(private readonly bundleRoot: string) {}
|
|
53
|
+
|
|
54
|
+
load(): boolean {
|
|
55
|
+
const graphPath = path.join(this.bundleRoot, "current", "graph.json");
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(graphPath, "utf8");
|
|
58
|
+
const data = JSON.parse(raw) as GraphData;
|
|
59
|
+
this.entities = data.entities ?? [];
|
|
60
|
+
this.edges = data.edges ?? [];
|
|
61
|
+
this.events = data.events ?? [];
|
|
62
|
+
|
|
63
|
+
this.entityById.clear();
|
|
64
|
+
this.entityByLabel.clear();
|
|
65
|
+
for (const e of this.entities) {
|
|
66
|
+
this.entityById.set(e.entity_id, e);
|
|
67
|
+
this.entityByLabel.set(e.label.toLowerCase(), e);
|
|
68
|
+
for (const alias of e.aliases) {
|
|
69
|
+
this.entityByLabel.set(alias.toLowerCase(), e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.graphMtime = fs.statSync(graphPath).mtimeMs;
|
|
73
|
+
this.loaded = true;
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
isLoaded(): boolean {
|
|
81
|
+
return this.loaded;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when the on-disk graph.json has changed since the last load().
|
|
86
|
+
* Always returns false when not yet loaded.
|
|
87
|
+
*/
|
|
88
|
+
isStale(): boolean {
|
|
89
|
+
if (!this.loaded) return false;
|
|
90
|
+
const graphPath = path.join(this.bundleRoot, "current", "graph.json");
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(graphPath).mtimeMs !== this.graphMtime;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Re-loads from disk if isStale(). Returns true when a reload actually happened.
|
|
100
|
+
*/
|
|
101
|
+
reloadIfStale(): boolean {
|
|
102
|
+
if (!this.isStale()) return false;
|
|
103
|
+
return this.load();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private findEntity(mention: string): BundleEntity | null {
|
|
107
|
+
const key = mention.toLowerCase().trim();
|
|
108
|
+
return this.entityByLabel.get(key) ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private findEntities(mentions: string[]): BundleEntity[] {
|
|
112
|
+
const found: BundleEntity[] = [];
|
|
113
|
+
for (const m of mentions) {
|
|
114
|
+
const e = this.findEntity(m);
|
|
115
|
+
if (e) found.push(e);
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
query(intent: QueryIntent): {
|
|
121
|
+
env: ResponseEnvelope | null;
|
|
122
|
+
errorClass: ErrorClass;
|
|
123
|
+
} {
|
|
124
|
+
if (!this.loaded) {
|
|
125
|
+
return { env: null, errorClass: "unavailable" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const anchors = this.findEntities(intent.anchor_mentions);
|
|
129
|
+
if (anchors.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
env: this.makeEnvelope(intent, [], [], "no matching entities found"),
|
|
132
|
+
errorClass: "ok",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const limit = intent.result_limit && intent.result_limit > 0 ? intent.result_limit : 10;
|
|
137
|
+
const candidates: QueryCandidate[] = [];
|
|
138
|
+
const groups: MemoryCandidateGroup[] = [];
|
|
139
|
+
|
|
140
|
+
for (const anchor of anchors) {
|
|
141
|
+
const relatedEdges = this.edges.filter((e) => {
|
|
142
|
+
const matchesHead = e.head_entity_id === anchor.entity_id;
|
|
143
|
+
const matchesTail = e.tail_entity_id === anchor.entity_id;
|
|
144
|
+
if (!matchesHead && !matchesTail) return false;
|
|
145
|
+
|
|
146
|
+
if (intent.relation_constraints?.length) {
|
|
147
|
+
return intent.relation_constraints.some((rc) => {
|
|
148
|
+
const clean = rc.replace(/\^-1$/, "").replace(/^\^/, "");
|
|
149
|
+
return e.relation === clean;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
for (const edge of relatedEdges) {
|
|
156
|
+
if (candidates.length >= limit) break;
|
|
157
|
+
const isHead = edge.head_entity_id === anchor.entity_id;
|
|
158
|
+
const otherId = isHead ? edge.tail_entity_id : edge.head_entity_id;
|
|
159
|
+
const other = this.entityById.get(otherId);
|
|
160
|
+
if (!other) continue;
|
|
161
|
+
|
|
162
|
+
if (intent.candidate_type) {
|
|
163
|
+
if (other.type.toLowerCase() !== intent.candidate_type.toLowerCase()) continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const score = other.mention_count + (other.distinct_session_count * 2);
|
|
167
|
+
|
|
168
|
+
candidates.push({
|
|
169
|
+
value: other.label,
|
|
170
|
+
score,
|
|
171
|
+
evidence: edge.evidence.slice(0, 200),
|
|
172
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
173
|
+
entity_id: other.entity_id,
|
|
174
|
+
scope: `via_${anchor.label}`,
|
|
175
|
+
support_count: other.mention_count,
|
|
176
|
+
distinct_session_count: other.distinct_session_count,
|
|
177
|
+
observed_path: [{
|
|
178
|
+
from_entity_id: anchor.entity_id,
|
|
179
|
+
from_label: anchor.label,
|
|
180
|
+
relation: edge.relation,
|
|
181
|
+
direction: isHead ? "forward" : "inverse",
|
|
182
|
+
to_entity_id: other.entity_id,
|
|
183
|
+
to_label: other.label,
|
|
184
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
185
|
+
}],
|
|
186
|
+
path_collision_count: 0,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
groups.push({
|
|
190
|
+
value: other.label,
|
|
191
|
+
score,
|
|
192
|
+
evidence: edge.evidence.slice(0, 200),
|
|
193
|
+
support_count: other.mention_count,
|
|
194
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
195
|
+
entity_ids: [other.entity_id],
|
|
196
|
+
scopes: [`via_${anchor.label}`],
|
|
197
|
+
via_relations: [edge.relation],
|
|
198
|
+
via_anchor_entity_ids: [anchor.entity_id],
|
|
199
|
+
observed_path: [{
|
|
200
|
+
from_entity_id: anchor.entity_id,
|
|
201
|
+
from_label: anchor.label,
|
|
202
|
+
relation: edge.relation,
|
|
203
|
+
direction: isHead ? "forward" : "inverse",
|
|
204
|
+
to_entity_id: other.entity_id,
|
|
205
|
+
to_label: other.label,
|
|
206
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
207
|
+
}],
|
|
208
|
+
path_collision_count: 0,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
214
|
+
groups.sort((a, b) => b.score - a.score);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
env: this.makeEnvelope(intent, candidates.slice(0, limit), groups.slice(0, limit)),
|
|
218
|
+
errorClass: "ok",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private makeEnvelope(
|
|
223
|
+
intent: QueryIntent,
|
|
224
|
+
candidates: QueryCandidate[],
|
|
225
|
+
groups: MemoryCandidateGroup[],
|
|
226
|
+
noDataReason?: string,
|
|
227
|
+
): ResponseEnvelope {
|
|
228
|
+
const manifestPath = path.join(this.bundleRoot, "current", "manifest.json");
|
|
229
|
+
let bundleVersion: string | undefined;
|
|
230
|
+
try {
|
|
231
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as { bundle_version?: string };
|
|
232
|
+
bundleVersion = m.bundle_version;
|
|
233
|
+
} catch { /* ok */ }
|
|
234
|
+
|
|
235
|
+
const memoryBlock: MemoryBlock = {
|
|
236
|
+
groups,
|
|
237
|
+
notes: [],
|
|
238
|
+
...(noDataReason ? { no_data_reason: noDataReason } : {}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
protocol_version: 1,
|
|
243
|
+
bundle_version: bundleVersion,
|
|
244
|
+
request_id: `local-${Date.now()}`,
|
|
245
|
+
candidates,
|
|
246
|
+
memory_block: memoryBlock,
|
|
247
|
+
warnings: [],
|
|
248
|
+
reason: candidates.length > 0 ? "ok" : "no_data",
|
|
249
|
+
latency_ms: 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/paths.ts
CHANGED