recallx-headless 1.0.7 → 1.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.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/app/cli/src/format.js +10 -0
- package/app/mcp/index.js +2 -2
- package/app/mcp/server.js +295 -39
- package/app/server/app.js +14 -12
- package/app/server/inferred-relations.js +23 -10
- package/app/server/observability.js +39 -14
- package/app/server/repositories.js +58 -16
- package/app/server/retrieval.js +84 -4
- package/app/server/workspace-session.js +1 -1
- package/app/shared/version.js +1 -1
- package/package.json +8 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jazpiper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -154,8 +154,10 @@ recallx workspace open --root /Users/name/Documents/RecallX-Test
|
|
|
154
154
|
- Default API base: `http://127.0.0.1:8787/api/v1`
|
|
155
155
|
- `recallx serve` starts the local RecallX API in-process from the installed package
|
|
156
156
|
- The CLI stays thin for day-to-day API operations and defers behavior to the HTTP API contract
|
|
157
|
+
- MCP tool results keep `structuredContent` as the authoritative payload; when possible the text mirror is a compact deterministic summary rather than a pretty JSON dump
|
|
157
158
|
- `--format json` is useful when scripting, while `--format markdown` is best for `context`
|
|
158
159
|
- `workspace open` switches the active workspace in the running local RecallX service without restarting the server
|
|
159
160
|
- `recallx-mcp` is the direct stdio MCP entrypoint from the npm package
|
|
161
|
+
- MCP tool results keep `structuredContent` authoritative and may render `content.text` as a compact deterministic summary instead of a pretty JSON mirror
|
|
160
162
|
- See the root [`README.md`](../../README.md) for source-run usage and install paths
|
|
161
163
|
- See [`docs/mcp.md`](../../docs/mcp.md) for editor MCP wiring details
|
package/app/cli/src/format.js
CHANGED
|
@@ -206,8 +206,18 @@ export function renderTelemetrySummary(data) {
|
|
|
206
206
|
`since: ${data?.since || ""}`,
|
|
207
207
|
`logs: ${data?.logsPath || ""}`,
|
|
208
208
|
`events: ${data?.totalEvents ?? 0}`,
|
|
209
|
+
`slow threshold: ${data?.slowRequestThresholdMs ?? ""}ms`,
|
|
209
210
|
];
|
|
210
211
|
|
|
212
|
+
const hot = Array.isArray(data?.hotOperations) ? data.hotOperations : [];
|
|
213
|
+
if (hot.length > 0) {
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push("Hot operations:");
|
|
216
|
+
for (const item of hot.slice(0, 5)) {
|
|
217
|
+
lines.push(`- [${item.surface}] ${item.operation} p95=${item.p95DurationMs ?? ""}ms errors=${item.errorCount}/${item.count}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
211
221
|
const slow = Array.isArray(data?.slowOperations) ? data.slowOperations : [];
|
|
212
222
|
if (slow.length > 0) {
|
|
213
223
|
lines.push("");
|
package/app/mcp/index.js
CHANGED
|
@@ -45,7 +45,7 @@ async function resolveObservabilityState() {
|
|
|
45
45
|
workspaceRoot: typeof workspace.rootPath === "string" ? workspace.rootPath : process.cwd(),
|
|
46
46
|
workspaceName: typeof workspace.workspaceName === "string" ? workspace.workspaceName : "RecallX MCP",
|
|
47
47
|
retentionDays: typeof values["observability.retentionDays"] === "number" ? values["observability.retentionDays"] : 14,
|
|
48
|
-
slowRequestMs: typeof values["observability.slowRequestMs"] === "number" ? values["observability.slowRequestMs"] :
|
|
48
|
+
slowRequestMs: typeof values["observability.slowRequestMs"] === "number" ? values["observability.slowRequestMs"] : 50,
|
|
49
49
|
capturePayloadShape: values["observability.capturePayloadShape"] !== false
|
|
50
50
|
};
|
|
51
51
|
}
|
|
@@ -55,7 +55,7 @@ async function resolveObservabilityState() {
|
|
|
55
55
|
workspaceRoot: process.cwd(),
|
|
56
56
|
workspaceName: "RecallX MCP",
|
|
57
57
|
retentionDays: 14,
|
|
58
|
-
slowRequestMs:
|
|
58
|
+
slowRequestMs: 50,
|
|
59
59
|
capturePayloadShape: true
|
|
60
60
|
};
|
|
61
61
|
}
|
package/app/mcp/server.js
CHANGED
|
@@ -64,8 +64,264 @@ function coerceBooleanSchema(defaultValue) {
|
|
|
64
64
|
return value;
|
|
65
65
|
}, z.boolean().default(defaultValue));
|
|
66
66
|
}
|
|
67
|
+
const textSummaryItemLimit = 5;
|
|
68
|
+
const textSummaryLength = 140;
|
|
69
|
+
const textTitleLength = 80;
|
|
70
|
+
function isRecord(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
function cleanInlineText(value, maxLength = textSummaryLength) {
|
|
74
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
75
|
+
if (compact.length <= maxLength) {
|
|
76
|
+
return compact;
|
|
77
|
+
}
|
|
78
|
+
return `${compact.slice(0, maxLength - 3).trimEnd()}...`;
|
|
79
|
+
}
|
|
80
|
+
function formatKeyLabel(key) {
|
|
81
|
+
return key
|
|
82
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
83
|
+
.replace(/[_-]+/g, " ")
|
|
84
|
+
.trim();
|
|
85
|
+
}
|
|
86
|
+
function capitalize(value) {
|
|
87
|
+
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
|
|
88
|
+
}
|
|
89
|
+
function pickString(record, keys) {
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const candidate = record[key];
|
|
92
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
function summarizeValue(value) {
|
|
99
|
+
if (typeof value === "string" && value.trim()) {
|
|
100
|
+
return cleanInlineText(value);
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return `${value.length} item(s)`;
|
|
107
|
+
}
|
|
108
|
+
if (isRecord(value)) {
|
|
109
|
+
const title = pickString(value, ["title", "name", "id", "nodeId"]);
|
|
110
|
+
if (title) {
|
|
111
|
+
return cleanInlineText(title);
|
|
112
|
+
}
|
|
113
|
+
return `${Object.keys(value).length} field(s)`;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
function unwrapSummaryRecord(record) {
|
|
118
|
+
const resultType = typeof record.resultType === "string" ? record.resultType : undefined;
|
|
119
|
+
if (resultType && isRecord(record[resultType])) {
|
|
120
|
+
const nested = record[resultType];
|
|
121
|
+
return {
|
|
122
|
+
kind: (resultType === "activity" ? pickString(nested, ["activityType"]) : undefined) ??
|
|
123
|
+
(resultType === "node" ? pickString(nested, ["type"]) : undefined) ??
|
|
124
|
+
resultType,
|
|
125
|
+
value: nested
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (isRecord(record.node)) {
|
|
129
|
+
return {
|
|
130
|
+
kind: resultType ?? "node",
|
|
131
|
+
value: record.node
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (isRecord(record.activity)) {
|
|
135
|
+
return {
|
|
136
|
+
kind: resultType ?? "activity",
|
|
137
|
+
value: record.activity
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind: pickString(record, ["type", "activityType", "status"]) ??
|
|
142
|
+
(typeof record.resultType === "string" ? record.resultType : undefined),
|
|
143
|
+
value: record
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function summarizeRecordLine(record, index) {
|
|
147
|
+
const { kind, value } = unwrapSummaryRecord(record);
|
|
148
|
+
const title = pickString(value, ["title", "targetNodeTitle", "name", "workspaceName", "rootPath", "id", "nodeId"]);
|
|
149
|
+
const identifier = pickString(value, ["id", "nodeId", "targetNodeId"]);
|
|
150
|
+
const detail = pickString(value, ["summary", "reason", "body", "message", "staleReason", "workspaceRoot"]);
|
|
151
|
+
const parts = [`${index}.`];
|
|
152
|
+
if (kind) {
|
|
153
|
+
parts.push(`[${kind}]`);
|
|
154
|
+
}
|
|
155
|
+
if (title) {
|
|
156
|
+
parts.push(cleanInlineText(title, textTitleLength));
|
|
157
|
+
}
|
|
158
|
+
if (identifier && identifier !== title) {
|
|
159
|
+
parts.push(`(id: ${cleanInlineText(identifier, textTitleLength)})`);
|
|
160
|
+
}
|
|
161
|
+
let line = parts.join(" ");
|
|
162
|
+
if (detail) {
|
|
163
|
+
line += ` - ${cleanInlineText(detail)}`;
|
|
164
|
+
}
|
|
165
|
+
return line;
|
|
166
|
+
}
|
|
167
|
+
function formatItemsSummary(payload) {
|
|
168
|
+
const rawItems = payload.items;
|
|
169
|
+
if (!Array.isArray(rawItems)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const items = rawItems.filter(isRecord);
|
|
173
|
+
const shown = Math.min(items.length, textSummaryItemLimit);
|
|
174
|
+
const total = typeof payload.total === "number" ? payload.total : items.length;
|
|
175
|
+
const lines = [`Results: ${shown} shown of ${total} total.`];
|
|
176
|
+
if (!items.length) {
|
|
177
|
+
lines.push("No results.");
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
for (const [index, item] of items.slice(0, textSummaryItemLimit).entries()) {
|
|
181
|
+
lines.push(summarizeRecordLine(item, index + 1));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (total > shown) {
|
|
185
|
+
lines.push(`More available: ${total - shown} additional result(s).`);
|
|
186
|
+
}
|
|
187
|
+
else if (items.length > shown) {
|
|
188
|
+
lines.push(`More available: ${items.length - shown} additional item(s).`);
|
|
189
|
+
}
|
|
190
|
+
if (typeof payload.nextCursor === "string" && payload.nextCursor.trim()) {
|
|
191
|
+
lines.push(`Next cursor: ${cleanInlineText(payload.nextCursor, textTitleLength)}`);
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
function formatBundleSummary(payload) {
|
|
196
|
+
const bundle = isRecord(payload.bundle) ? payload.bundle : payload;
|
|
197
|
+
if (!isRecord(payload.bundle) && !isRecord(bundle.target)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const target = isRecord(bundle.target) ? bundle.target : null;
|
|
201
|
+
const type = target ? pickString(target, ["type"]) : undefined;
|
|
202
|
+
const mode = pickString(bundle, ["mode"]);
|
|
203
|
+
const preset = pickString(bundle, ["preset"]);
|
|
204
|
+
const summary = pickString(bundle, ["summary"]);
|
|
205
|
+
const items = Array.isArray(bundle.items) ? bundle.items.filter(isRecord) : [];
|
|
206
|
+
const decisions = Array.isArray(bundle.decisions) ? bundle.decisions : [];
|
|
207
|
+
const openQuestions = Array.isArray(bundle.openQuestions) ? bundle.openQuestions : [];
|
|
208
|
+
const activityDigest = Array.isArray(bundle.activityDigest) ? bundle.activityDigest : [];
|
|
209
|
+
const lines = [`Context bundle: Target${type ? ` [${type}]` : ""}.`];
|
|
210
|
+
if (mode || preset) {
|
|
211
|
+
lines.push(`Mode: ${[mode, preset].filter(Boolean).join(", ")}.`);
|
|
212
|
+
}
|
|
213
|
+
if (summary) {
|
|
214
|
+
lines.push(`Summary: ${cleanInlineText(summary)}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push(`Items: ${items.length}.`);
|
|
217
|
+
for (const [index, item] of items.slice(0, textSummaryItemLimit).entries()) {
|
|
218
|
+
lines.push(summarizeRecordLine(item, index + 1));
|
|
219
|
+
}
|
|
220
|
+
if (items.length > textSummaryItemLimit) {
|
|
221
|
+
lines.push(`More bundle items: ${items.length - textSummaryItemLimit}.`);
|
|
222
|
+
}
|
|
223
|
+
if (activityDigest.length) {
|
|
224
|
+
lines.push(`Activity digest: ${activityDigest.length} entr${activityDigest.length === 1 ? "y" : "ies"}.`);
|
|
225
|
+
}
|
|
226
|
+
if (decisions.length) {
|
|
227
|
+
lines.push(`Decisions: ${decisions.length}.`);
|
|
228
|
+
}
|
|
229
|
+
if (openQuestions.length) {
|
|
230
|
+
lines.push(`Open questions: ${openQuestions.length}.`);
|
|
231
|
+
}
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
234
|
+
function formatWriteSummary(payload) {
|
|
235
|
+
const primaryKey = ["node", "activity", "relation"].find((key) => isRecord(payload[key]));
|
|
236
|
+
if (!primaryKey && !isRecord(payload.landing)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const lines = [];
|
|
240
|
+
if (primaryKey && isRecord(payload[primaryKey])) {
|
|
241
|
+
lines.push(`${capitalize(primaryKey)} stored: ${summarizeRecordLine(payload[primaryKey], 1).replace(/^1\.\s*/, "")}`);
|
|
242
|
+
}
|
|
243
|
+
if (isRecord(payload.landing)) {
|
|
244
|
+
const landing = payload.landing;
|
|
245
|
+
const parts = [
|
|
246
|
+
typeof landing.storedAs === "string" ? `storedAs=${landing.storedAs}` : null,
|
|
247
|
+
typeof landing.canonicality === "string" ? `canonicality=${landing.canonicality}` : null,
|
|
248
|
+
typeof landing.status === "string" ? `status=${landing.status}` : null,
|
|
249
|
+
typeof landing.governanceState === "string" ? `governance=${landing.governanceState}` : null
|
|
250
|
+
].filter((value) => Boolean(value));
|
|
251
|
+
if (parts.length) {
|
|
252
|
+
lines.push(`Landing: ${parts.join(", ")}.`);
|
|
253
|
+
}
|
|
254
|
+
if (typeof landing.reason === "string" && landing.reason.trim()) {
|
|
255
|
+
lines.push(`Reason: ${cleanInlineText(landing.reason)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return lines.length ? lines.join("\n") : null;
|
|
259
|
+
}
|
|
260
|
+
function formatObjectSummary(payload) {
|
|
261
|
+
const preferredKeys = [
|
|
262
|
+
"status",
|
|
263
|
+
"message",
|
|
264
|
+
"workspaceName",
|
|
265
|
+
"workspaceRoot",
|
|
266
|
+
"rootPath",
|
|
267
|
+
"schemaVersion",
|
|
268
|
+
"authMode",
|
|
269
|
+
"bindAddress",
|
|
270
|
+
"queued",
|
|
271
|
+
"queuedCount",
|
|
272
|
+
"nodeId",
|
|
273
|
+
"id",
|
|
274
|
+
"title",
|
|
275
|
+
"type",
|
|
276
|
+
"summary",
|
|
277
|
+
"reason",
|
|
278
|
+
"nextCursor"
|
|
279
|
+
];
|
|
280
|
+
const orderedKeys = Array.from(new Set([...preferredKeys.filter((key) => key in payload), ...Object.keys(payload).filter((key) => !preferredKeys.includes(key))]));
|
|
281
|
+
const lines = [];
|
|
282
|
+
for (const key of orderedKeys) {
|
|
283
|
+
const summary = summarizeValue(payload[key]);
|
|
284
|
+
if (!summary) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
lines.push(`${formatKeyLabel(key)}: ${summary}`);
|
|
288
|
+
if (lines.length >= 8) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return lines.length ? lines.join("\n") : "Structured response returned.";
|
|
293
|
+
}
|
|
294
|
+
function formatArraySummary(items) {
|
|
295
|
+
const recordItems = items.filter(isRecord);
|
|
296
|
+
if (!recordItems.length) {
|
|
297
|
+
return `Items: ${items.length}.`;
|
|
298
|
+
}
|
|
299
|
+
const lines = [`Items: ${recordItems.length}.`];
|
|
300
|
+
for (const [index, item] of recordItems.slice(0, textSummaryItemLimit).entries()) {
|
|
301
|
+
lines.push(summarizeRecordLine(item, index + 1));
|
|
302
|
+
}
|
|
303
|
+
if (recordItems.length > textSummaryItemLimit) {
|
|
304
|
+
lines.push(`More available: ${recordItems.length - textSummaryItemLimit} additional item(s).`);
|
|
305
|
+
}
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|
|
67
308
|
function formatStructuredContent(content) {
|
|
68
|
-
|
|
309
|
+
if (Array.isArray(content)) {
|
|
310
|
+
return formatArraySummary(content);
|
|
311
|
+
}
|
|
312
|
+
if (isRecord(content)) {
|
|
313
|
+
return formatBundleSummary(content) ?? formatItemsSummary(content) ?? formatWriteSummary(content) ?? formatObjectSummary(content);
|
|
314
|
+
}
|
|
315
|
+
if (typeof content === "string" && content.trim()) {
|
|
316
|
+
return cleanInlineText(content);
|
|
317
|
+
}
|
|
318
|
+
if (typeof content === "number" || typeof content === "boolean") {
|
|
319
|
+
return String(content);
|
|
320
|
+
}
|
|
321
|
+
return "Structured response returned.";
|
|
322
|
+
}
|
|
323
|
+
function renderToolText(_toolName, structuredContent) {
|
|
324
|
+
return formatStructuredContent(structuredContent);
|
|
69
325
|
}
|
|
70
326
|
function formatInvalidBundleModeMessage(input) {
|
|
71
327
|
const quotedInput = typeof input === "string" && input.trim() ? `'${input}'` : "that value";
|
|
@@ -85,12 +341,12 @@ function bundlePresetSchema(defaultValue) {
|
|
|
85
341
|
error: (issue) => formatInvalidBundlePresetMessage(issue.input)
|
|
86
342
|
})).default(defaultValue);
|
|
87
343
|
}
|
|
88
|
-
function toolResult(structuredContent) {
|
|
344
|
+
function toolResult(toolName, structuredContent) {
|
|
89
345
|
return {
|
|
90
346
|
content: [
|
|
91
347
|
{
|
|
92
348
|
type: "text",
|
|
93
|
-
text:
|
|
349
|
+
text: renderToolText(toolName, structuredContent)
|
|
94
350
|
}
|
|
95
351
|
],
|
|
96
352
|
structuredContent
|
|
@@ -133,14 +389,14 @@ const readOnlyToolAnnotations = {
|
|
|
133
389
|
readOnlyHint: true,
|
|
134
390
|
idempotentHint: true
|
|
135
391
|
};
|
|
136
|
-
function createGetToolHandler(apiClient, path) {
|
|
137
|
-
return async () => toolResult(await apiClient.get(path));
|
|
392
|
+
function createGetToolHandler(toolName, apiClient, path) {
|
|
393
|
+
return async () => toolResult(toolName, await apiClient.get(path));
|
|
138
394
|
}
|
|
139
|
-
function createPostToolHandler(apiClient, path) {
|
|
140
|
-
return async (input) => toolResult(await apiClient.post(path, input));
|
|
395
|
+
function createPostToolHandler(toolName, apiClient, path) {
|
|
396
|
+
return async (input) => toolResult(toolName, await apiClient.post(path, input));
|
|
141
397
|
}
|
|
142
|
-
function createNormalizedPostToolHandler(apiClient, path, normalize) {
|
|
143
|
-
return async (input) => toolResult(await apiClient.post(path, normalize(input)));
|
|
398
|
+
function createNormalizedPostToolHandler(toolName, apiClient, path, normalize) {
|
|
399
|
+
return async (input) => toolResult(toolName, await apiClient.post(path, normalize(input)));
|
|
144
400
|
}
|
|
145
401
|
function withReadOnlyAnnotations(config) {
|
|
146
402
|
return {
|
|
@@ -312,7 +568,7 @@ export function createRecallXMcpServer(params) {
|
|
|
312
568
|
workspaceRoot: process.cwd(),
|
|
313
569
|
workspaceName: "RecallX MCP",
|
|
314
570
|
retentionDays: 14,
|
|
315
|
-
slowRequestMs:
|
|
571
|
+
slowRequestMs: 50,
|
|
316
572
|
capturePayloadShape: true
|
|
317
573
|
};
|
|
318
574
|
let currentObservabilityState = params?.observabilityState ?? defaultObservabilityState;
|
|
@@ -335,7 +591,7 @@ export function createRecallXMcpServer(params) {
|
|
|
335
591
|
name: "recallx-mcp",
|
|
336
592
|
version: params?.serverVersion ?? RECALLX_VERSION
|
|
337
593
|
}, {
|
|
338
|
-
instructions: "Use RecallX as
|
|
594
|
+
instructions: "Use RecallX as the default local memory layer for meaningful work, not as an afterthought. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. Before making assumptions or starting a meaningful task, read context first: confirm the active workspace, use recallx_search_workspace as the broad default when the target is still unclear, narrow with recallx_search_nodes or recallx_search_activities when needed, and build a compact recallx_context_bundle before deep execution when a node or project is known. When the work is clearly project-shaped, search for an existing project inside the current workspace first, create one only if no suitable project exists, and then anchor follow-up context and routine logs to that project. Once a project is known, do not keep writing untargeted workspace captures for routine work logs: append activity to that project or pass targetNodeId on capture writes. Reserve workspace-scope inbox activity for genuinely untargeted, cross-project, or not-yet-classified short logs. Prefer read tools before durable writes, prefer compact context over repeated broad browsing, and write back concise summaries, decisions, or feedback when RecallX materially helped the task. Include source details on durable writes when you want caller-specific provenance.",
|
|
339
595
|
capabilities: {
|
|
340
596
|
logging: {}
|
|
341
597
|
}
|
|
@@ -388,12 +644,12 @@ export function createRecallXMcpServer(params) {
|
|
|
388
644
|
includeDetails: z.boolean().optional().default(true)
|
|
389
645
|
}),
|
|
390
646
|
outputSchema: healthOutputSchema
|
|
391
|
-
}, async () => toolResult(await apiClient.get("/health")));
|
|
647
|
+
}, async () => toolResult("recallx_health", await apiClient.get("/health")));
|
|
392
648
|
registerReadOnlyTool(server, "recallx_workspace_current", {
|
|
393
649
|
title: "Current Workspace",
|
|
394
650
|
description: "Read the currently active RecallX workspace and auth mode. Use this to confirm the default workspace scope before deciding whether an explicit user request justifies switching workspaces.",
|
|
395
651
|
outputSchema: workspaceInfoSchema
|
|
396
|
-
}, createGetToolHandler(apiClient, "/workspace"));
|
|
652
|
+
}, createGetToolHandler("recallx_workspace_current", apiClient, "/workspace"));
|
|
397
653
|
registerReadOnlyTool(server, "recallx_workspace_list", {
|
|
398
654
|
title: "List Workspaces",
|
|
399
655
|
description: "List known RecallX workspaces and identify the currently active one.",
|
|
@@ -401,7 +657,7 @@ export function createRecallXMcpServer(params) {
|
|
|
401
657
|
current: workspaceInfoSchema,
|
|
402
658
|
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() }))
|
|
403
659
|
})
|
|
404
|
-
}, createGetToolHandler(apiClient, "/workspaces"));
|
|
660
|
+
}, createGetToolHandler("recallx_workspace_list", apiClient, "/workspaces"));
|
|
405
661
|
registerTool(server, "recallx_workspace_create", {
|
|
406
662
|
title: "Create Workspace",
|
|
407
663
|
description: "Create a RecallX workspace on disk and switch the running service to it without restarting. Only use this when the user explicitly requests creating or switching to a new workspace.",
|
|
@@ -409,14 +665,14 @@ export function createRecallXMcpServer(params) {
|
|
|
409
665
|
rootPath: z.string().min(1).describe("Absolute or user-resolved path for the new workspace root."),
|
|
410
666
|
workspaceName: z.string().min(1).optional().describe("Human-friendly workspace name.")
|
|
411
667
|
}
|
|
412
|
-
}, createPostToolHandler(apiClient, "/workspaces"));
|
|
668
|
+
}, createPostToolHandler("recallx_workspace_create", apiClient, "/workspaces"));
|
|
413
669
|
registerTool(server, "recallx_workspace_open", {
|
|
414
670
|
title: "Open Workspace",
|
|
415
671
|
description: "Switch the running RecallX service to another existing workspace. Only use this when the user explicitly requests opening or switching workspaces.",
|
|
416
672
|
inputSchema: {
|
|
417
673
|
rootPath: z.string().min(1).describe("Existing workspace root path to open.")
|
|
418
674
|
}
|
|
419
|
-
}, createPostToolHandler(apiClient, "/workspaces/open"));
|
|
675
|
+
}, createPostToolHandler("recallx_workspace_open", apiClient, "/workspaces/open"));
|
|
420
676
|
registerReadOnlyTool(server, "recallx_semantic_status", {
|
|
421
677
|
title: "Semantic Index Status",
|
|
422
678
|
description: "Read the current semantic indexing status, provider configuration, and queued item counts.",
|
|
@@ -434,7 +690,7 @@ export function createRecallXMcpServer(params) {
|
|
|
434
690
|
failed: z.number()
|
|
435
691
|
})
|
|
436
692
|
})
|
|
437
|
-
}, createGetToolHandler(apiClient, "/semantic/status"));
|
|
693
|
+
}, createGetToolHandler("recallx_semantic_status", apiClient, "/semantic/status"));
|
|
438
694
|
registerReadOnlyTool(server, "recallx_semantic_issues", {
|
|
439
695
|
title: "Semantic Index Issues",
|
|
440
696
|
description: "Read semantic indexing issues with optional status filters and cursor pagination.",
|
|
@@ -462,7 +718,7 @@ export function createRecallXMcpServer(params) {
|
|
|
462
718
|
if (statuses?.length) {
|
|
463
719
|
params.set("statuses", statuses.join(","));
|
|
464
720
|
}
|
|
465
|
-
return toolResult(await apiClient.get(`/semantic/issues?${params.toString()}`));
|
|
721
|
+
return toolResult("recallx_semantic_issues", await apiClient.get(`/semantic/issues?${params.toString()}`));
|
|
466
722
|
});
|
|
467
723
|
registerReadOnlyTool(server, "recallx_search_nodes", {
|
|
468
724
|
title: "Search Nodes",
|
|
@@ -486,7 +742,7 @@ export function createRecallXMcpServer(params) {
|
|
|
486
742
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
487
743
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
488
744
|
}
|
|
489
|
-
}, createNormalizedPostToolHandler(apiClient, "/nodes/search", normalizeNodeSearchInput));
|
|
745
|
+
}, createNormalizedPostToolHandler("recallx_search_nodes", apiClient, "/nodes/search", normalizeNodeSearchInput));
|
|
490
746
|
registerReadOnlyTool(server, "recallx_search_activities", {
|
|
491
747
|
title: "Search Activities",
|
|
492
748
|
description: "Search operational activity timelines by keyword and optional filters. Prefer this for recent logs, change history, and 'what happened recently' questions. Accepts `activityType` and `targetNodeId` aliases and normalizes single strings into arrays.",
|
|
@@ -508,7 +764,7 @@ export function createRecallXMcpServer(params) {
|
|
|
508
764
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
509
765
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
510
766
|
}
|
|
511
|
-
}, createNormalizedPostToolHandler(apiClient, "/activities/search", normalizeActivitySearchInput));
|
|
767
|
+
}, createNormalizedPostToolHandler("recallx_search_activities", apiClient, "/activities/search", normalizeActivitySearchInput));
|
|
512
768
|
registerReadOnlyTool(server, "recallx_search_workspace", {
|
|
513
769
|
title: "Search Workspace",
|
|
514
770
|
description: "Search nodes, activities, or both through one workspace-wide endpoint. This is the preferred broad entry point when the target node or request shape is still unclear, or when you need both node and activity recall in the current workspace. Use `scopes` as an array such as `[\"nodes\", \"activities\"]`, or use `scope: \"activities\"` for a single scope. Do not pass a comma-separated string like `\"nodes,activities\"`.",
|
|
@@ -538,14 +794,14 @@ export function createRecallXMcpServer(params) {
|
|
|
538
794
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
539
795
|
sort: z.enum(["relevance", "updated_at", "smart"]).default("relevance")
|
|
540
796
|
}
|
|
541
|
-
}, createNormalizedPostToolHandler(apiClient, "/search", normalizeWorkspaceSearchInput));
|
|
797
|
+
}, createNormalizedPostToolHandler("recallx_search_workspace", apiClient, "/search", normalizeWorkspaceSearchInput));
|
|
542
798
|
registerReadOnlyTool(server, "recallx_get_node", {
|
|
543
799
|
title: "Get Node",
|
|
544
800
|
description: "Fetch a node together with its related nodes, activities, artifacts, and provenance.",
|
|
545
801
|
inputSchema: {
|
|
546
802
|
nodeId: z.string().min(1).describe("Target node id.")
|
|
547
803
|
}
|
|
548
|
-
}, async ({ nodeId }) => toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
|
|
804
|
+
}, async ({ nodeId }) => toolResult("recallx_get_node", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
|
|
549
805
|
registerReadOnlyTool(server, "recallx_get_related", {
|
|
550
806
|
title: "Get Node Neighborhood",
|
|
551
807
|
description: "Fetch the canonical RecallX node neighborhood with optional inferred relations.",
|
|
@@ -565,7 +821,7 @@ export function createRecallXMcpServer(params) {
|
|
|
565
821
|
if (relationTypeFilter.length) {
|
|
566
822
|
query.set("types", relationTypeFilter.join(","));
|
|
567
823
|
}
|
|
568
|
-
return toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
824
|
+
return toolResult("recallx_get_related", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
569
825
|
});
|
|
570
826
|
registerTool(server, "recallx_upsert_inferred_relation", {
|
|
571
827
|
title: "Upsert Inferred Relation",
|
|
@@ -583,7 +839,7 @@ export function createRecallXMcpServer(params) {
|
|
|
583
839
|
expiresAt: z.string().optional(),
|
|
584
840
|
metadata: jsonRecordSchema
|
|
585
841
|
}
|
|
586
|
-
}, createPostToolHandler(apiClient, "/inferred-relations"));
|
|
842
|
+
}, createPostToolHandler("recallx_upsert_inferred_relation", apiClient, "/inferred-relations"));
|
|
587
843
|
registerTool(server, "recallx_append_relation_usage_event", {
|
|
588
844
|
title: "Append Relation Usage Event",
|
|
589
845
|
description: "Append a lightweight usage signal after a relation actually helped retrieval or final output.",
|
|
@@ -597,7 +853,7 @@ export function createRecallXMcpServer(params) {
|
|
|
597
853
|
delta: z.number(),
|
|
598
854
|
metadata: jsonRecordSchema
|
|
599
855
|
}
|
|
600
|
-
}, createPostToolHandler(apiClient, "/relation-usage-events"));
|
|
856
|
+
}, createPostToolHandler("recallx_append_relation_usage_event", apiClient, "/relation-usage-events"));
|
|
601
857
|
registerTool(server, "recallx_append_search_feedback", {
|
|
602
858
|
title: "Append Search Feedback",
|
|
603
859
|
description: "Append a usefulness signal for a node or activity search result after it helped or failed a task.",
|
|
@@ -612,7 +868,7 @@ export function createRecallXMcpServer(params) {
|
|
|
612
868
|
confidence: z.number().min(0).max(1).default(1),
|
|
613
869
|
metadata: jsonRecordSchema
|
|
614
870
|
}
|
|
615
|
-
}, createPostToolHandler(apiClient, "/search-feedback-events"));
|
|
871
|
+
}, createPostToolHandler("recallx_append_search_feedback", apiClient, "/search-feedback-events"));
|
|
616
872
|
registerTool(server, "recallx_recompute_inferred_relations", {
|
|
617
873
|
title: "Recompute Inferred Relations",
|
|
618
874
|
description: "Run an explicit maintenance pass that refreshes inferred relation usage_score and final_score from usage events.",
|
|
@@ -621,7 +877,7 @@ export function createRecallXMcpServer(params) {
|
|
|
621
877
|
generator: z.string().min(1).optional(),
|
|
622
878
|
limit: z.number().int().min(1).max(500).default(100)
|
|
623
879
|
}
|
|
624
|
-
}, createPostToolHandler(apiClient, "/inferred-relations/recompute"));
|
|
880
|
+
}, createPostToolHandler("recallx_recompute_inferred_relations", apiClient, "/inferred-relations/recompute"));
|
|
625
881
|
registerTool(server, "recallx_append_activity", {
|
|
626
882
|
title: "Append Activity",
|
|
627
883
|
description: "Append an activity entry to a specific RecallX node or project timeline with provenance. Use this when you already know the target node or project; otherwise prefer recallx_capture_memory for general workspace-scope updates.",
|
|
@@ -632,7 +888,7 @@ export function createRecallXMcpServer(params) {
|
|
|
632
888
|
source: sourceSchema,
|
|
633
889
|
metadata: jsonRecordSchema
|
|
634
890
|
}
|
|
635
|
-
}, createPostToolHandler(apiClient, "/activities"));
|
|
891
|
+
}, createPostToolHandler("recallx_append_activity", apiClient, "/activities"));
|
|
636
892
|
registerTool(server, "recallx_capture_memory", {
|
|
637
893
|
title: "Capture Memory",
|
|
638
894
|
description: "Safely capture a memory item without choosing low-level storage first. Prefer this as the default write only when the conversation is not yet tied to a specific project or node. Once a project or target node is known, include targetNodeId or switch to recallx_append_activity for routine work logs. General short logs can stay at workspace scope and be auto-routed into activities, while reusable content can still land as durable memory.",
|
|
@@ -646,7 +902,7 @@ export function createRecallXMcpServer(params) {
|
|
|
646
902
|
source: sourceSchema,
|
|
647
903
|
metadata: jsonRecordSchema
|
|
648
904
|
}
|
|
649
|
-
}, createPostToolHandler(apiClient, "/capture"));
|
|
905
|
+
}, createPostToolHandler("recallx_capture_memory", apiClient, "/capture"));
|
|
650
906
|
registerTool(server, "recallx_create_node", {
|
|
651
907
|
title: "Create Node",
|
|
652
908
|
description: "Create a durable RecallX node with provenance. Use this for reusable knowledge; when creating a project node in the current workspace, search first and only create one if no suitable project already exists. Short work-log updates are usually better captured with `recallx_capture_memory` or `recallx_append_activity`.",
|
|
@@ -663,7 +919,7 @@ export function createRecallXMcpServer(params) {
|
|
|
663
919
|
}
|
|
664
920
|
}, async (input) => {
|
|
665
921
|
try {
|
|
666
|
-
return toolResult(await apiClient.post("/nodes", input));
|
|
922
|
+
return toolResult("recallx_create_node", await apiClient.post("/nodes", input));
|
|
667
923
|
}
|
|
668
924
|
catch (error) {
|
|
669
925
|
if (error instanceof RecallXApiError &&
|
|
@@ -695,7 +951,7 @@ export function createRecallXMcpServer(params) {
|
|
|
695
951
|
.min(1)
|
|
696
952
|
.max(100)
|
|
697
953
|
}
|
|
698
|
-
}, async (input) => toolResult(await apiClient.post("/nodes/batch", input)));
|
|
954
|
+
}, async (input) => toolResult("recallx_create_nodes", await apiClient.post("/nodes/batch", input)));
|
|
699
955
|
registerTool(server, "recallx_create_relation", {
|
|
700
956
|
title: "Create Relation",
|
|
701
957
|
description: "Create a relation between two nodes. Agent-created relations typically start suggested and are promoted automatically when confidence improves.",
|
|
@@ -707,7 +963,7 @@ export function createRecallXMcpServer(params) {
|
|
|
707
963
|
source: sourceSchema,
|
|
708
964
|
metadata: jsonRecordSchema
|
|
709
965
|
}
|
|
710
|
-
}, createPostToolHandler(apiClient, "/relations"));
|
|
966
|
+
}, createPostToolHandler("recallx_create_relation", apiClient, "/relations"));
|
|
711
967
|
registerReadOnlyTool(server, "recallx_list_governance_issues", {
|
|
712
968
|
title: "List Governance Issues",
|
|
713
969
|
description: "List contested or low-confidence governance items that may need inspection.",
|
|
@@ -720,7 +976,7 @@ export function createRecallXMcpServer(params) {
|
|
|
720
976
|
states: states.join(","),
|
|
721
977
|
limit: String(limit)
|
|
722
978
|
});
|
|
723
|
-
return toolResult(await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
979
|
+
return toolResult("recallx_list_governance_issues", await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
724
980
|
});
|
|
725
981
|
registerReadOnlyTool(server, "recallx_get_governance_state", {
|
|
726
982
|
title: "Get Governance State",
|
|
@@ -729,7 +985,7 @@ export function createRecallXMcpServer(params) {
|
|
|
729
985
|
entityType: z.enum(["node", "relation"]),
|
|
730
986
|
entityId: z.string().min(1)
|
|
731
987
|
}
|
|
732
|
-
}, async ({ entityType, entityId }) => toolResult(await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
|
|
988
|
+
}, async ({ entityType, entityId }) => toolResult("recallx_get_governance_state", await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
|
|
733
989
|
registerTool(server, "recallx_recompute_governance", {
|
|
734
990
|
title: "Recompute Governance",
|
|
735
991
|
description: "Run a bounded automatic governance recompute pass for nodes, relations, or both.",
|
|
@@ -738,7 +994,7 @@ export function createRecallXMcpServer(params) {
|
|
|
738
994
|
entityIds: z.array(z.string().min(1)).max(200).optional(),
|
|
739
995
|
limit: z.number().int().min(1).max(500).default(100)
|
|
740
996
|
}
|
|
741
|
-
}, createPostToolHandler(apiClient, "/governance/recompute"));
|
|
997
|
+
}, createPostToolHandler("recallx_recompute_governance", apiClient, "/governance/recompute"));
|
|
742
998
|
registerReadOnlyTool(server, "recallx_context_bundle", {
|
|
743
999
|
title: "Build Context Bundle",
|
|
744
1000
|
description: "Build a compact RecallX context bundle for coding, research, writing, or decision support. Omit targetId to get a workspace-entry bundle when the work is not yet tied to a specific project or node, and add targetId only after you know which project or node should anchor the context.",
|
|
@@ -766,7 +1022,7 @@ export function createRecallXMcpServer(params) {
|
|
|
766
1022
|
maxItems: 10
|
|
767
1023
|
})
|
|
768
1024
|
}
|
|
769
|
-
}, async ({ targetId, ...input }) => toolResult(await apiClient.post("/context/bundles", {
|
|
1025
|
+
}, async ({ targetId, ...input }) => toolResult("recallx_context_bundle", await apiClient.post("/context/bundles", {
|
|
770
1026
|
...input,
|
|
771
1027
|
...(targetId
|
|
772
1028
|
? {
|
|
@@ -782,14 +1038,14 @@ export function createRecallXMcpServer(params) {
|
|
|
782
1038
|
inputSchema: {
|
|
783
1039
|
limit: coerceIntegerSchema(250, 1, 1000)
|
|
784
1040
|
}
|
|
785
|
-
}, createPostToolHandler(apiClient, "/semantic/reindex"));
|
|
1041
|
+
}, createPostToolHandler("recallx_semantic_reindex", apiClient, "/semantic/reindex"));
|
|
786
1042
|
registerTool(server, "recallx_semantic_reindex_node", {
|
|
787
1043
|
title: "Queue Node Semantic Reindex",
|
|
788
1044
|
description: "Queue semantic reindexing for a specific node id.",
|
|
789
1045
|
inputSchema: {
|
|
790
1046
|
nodeId: z.string().min(1)
|
|
791
1047
|
}
|
|
792
|
-
}, async ({ nodeId }) => toolResult(await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {})));
|
|
1048
|
+
}, async ({ nodeId }) => toolResult("recallx_semantic_reindex_node", await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {})));
|
|
793
1049
|
registerReadOnlyTool(server, "recallx_rank_candidates", {
|
|
794
1050
|
title: "Rank Candidate Nodes",
|
|
795
1051
|
description: "Rank a bounded set of candidate node ids for a target using RecallX request-time retrieval scoring.",
|
|
@@ -799,6 +1055,6 @@ export function createRecallXMcpServer(params) {
|
|
|
799
1055
|
preset: bundlePresetSchema("for-assistant"),
|
|
800
1056
|
targetNodeId: z.string().optional()
|
|
801
1057
|
}
|
|
802
|
-
}, createPostToolHandler(apiClient, "/retrieval/rank-candidates"));
|
|
1058
|
+
}, createPostToolHandler("recallx_rank_candidates", apiClient, "/retrieval/rank-candidates"));
|
|
803
1059
|
return server;
|
|
804
1060
|
}
|
package/app/server/app.js
CHANGED
|
@@ -9,7 +9,7 @@ import { AppError } from "./errors.js";
|
|
|
9
9
|
import { isShortLogLikeAgentNodeInput, maybeCreatePromotionCandidate, recomputeAutomaticGovernance, resolveGovernancePolicy, resolveNodeGovernance, resolveRelationStatus, shouldPromoteActivitySummary } from "./governance.js";
|
|
10
10
|
import { refreshAutomaticInferredRelationsForNode, reindexAutomaticInferredRelations } from "./inferred-relations.js";
|
|
11
11
|
import { appendCurrentTelemetryDetails, createObservabilityWriter, summarizePayloadShape } from "./observability.js";
|
|
12
|
-
import { buildSemanticCandidateBonusMap, buildCandidateRelationBonusMap, buildContextBundle, buildNeighborhoodItems, buildTargetRelatedRetrievalItems, bundleAsMarkdown, computeRankCandidateScore, shouldUseSemanticCandidateAugmentation } from "./retrieval.js";
|
|
12
|
+
import { buildSemanticCandidateBonusMap, buildCandidateRelationBonusMap, buildContextBundle, buildNeighborhoodItems, buildNeighborhoodItemsBatch, buildTargetRelatedRetrievalItems, bundleAsMarkdown, computeRankCandidateScore, selectSemanticCandidateIds, shouldUseSemanticCandidateAugmentation } from "./retrieval.js";
|
|
13
13
|
import { buildProjectGraph } from "./project-graph.js";
|
|
14
14
|
import { createId, isPathWithinRoot } from "./utils.js";
|
|
15
15
|
const relationTypeSet = new Set(relationTypes);
|
|
@@ -546,7 +546,7 @@ export function createRecallXApp(params) {
|
|
|
546
546
|
workspaceRoot: currentWorkspaceRoot(),
|
|
547
547
|
workspaceName: currentWorkspaceInfo().workspaceName,
|
|
548
548
|
retentionDays: Math.max(1, parseNumberSetting(settings["observability.retentionDays"], 14)),
|
|
549
|
-
slowRequestMs: Math.max(1, parseNumberSetting(settings["observability.slowRequestMs"],
|
|
549
|
+
slowRequestMs: Math.max(1, parseNumberSetting(settings["observability.slowRequestMs"], 50)),
|
|
550
550
|
capturePayloadShape: parseBooleanSetting(settings["observability.capturePayloadShape"], true)
|
|
551
551
|
};
|
|
552
552
|
};
|
|
@@ -1779,19 +1779,17 @@ export function createRecallXApp(params) {
|
|
|
1779
1779
|
}, (span) => {
|
|
1780
1780
|
const repository = currentRepository();
|
|
1781
1781
|
const nodeId = readRequestParam(request.params.id);
|
|
1782
|
-
const
|
|
1782
|
+
const neighborhoodOptions = {
|
|
1783
1783
|
relationTypes: types,
|
|
1784
1784
|
includeInferred,
|
|
1785
1785
|
maxInferred
|
|
1786
|
-
}
|
|
1786
|
+
};
|
|
1787
|
+
const result = buildNeighborhoodItems(repository, nodeId, neighborhoodOptions);
|
|
1787
1788
|
const expanded = depth === 2
|
|
1788
1789
|
? (() => {
|
|
1789
1790
|
const seen = new Set(result.map((item) => `${item.edge.relationId}:${item.node.id}:1`));
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
includeInferred,
|
|
1793
|
-
maxInferred
|
|
1794
|
-
})
|
|
1791
|
+
const secondHopByNodeId = buildNeighborhoodItemsBatch(repository, result.map((item) => item.node.id), neighborhoodOptions);
|
|
1792
|
+
const secondHop = result.flatMap((item) => (secondHopByNodeId.get(item.node.id) ?? [])
|
|
1795
1793
|
.filter((nested) => nested.node.id !== nodeId)
|
|
1796
1794
|
.map((nested) => ({
|
|
1797
1795
|
...nested,
|
|
@@ -1814,7 +1812,8 @@ export function createRecallXApp(params) {
|
|
|
1814
1812
|
})()
|
|
1815
1813
|
: result;
|
|
1816
1814
|
span.addDetails({
|
|
1817
|
-
resultCount: expanded.length
|
|
1815
|
+
resultCount: expanded.length,
|
|
1816
|
+
firstHopCount: result.length
|
|
1818
1817
|
});
|
|
1819
1818
|
return expanded;
|
|
1820
1819
|
});
|
|
@@ -2154,8 +2153,9 @@ export function createRecallXApp(params) {
|
|
|
2154
2153
|
});
|
|
2155
2154
|
const semanticAugmentation = repository.getSemanticAugmentationSettings();
|
|
2156
2155
|
const semanticEnabled = shouldUseSemanticCandidateAugmentation(query, candidates);
|
|
2156
|
+
const semanticCandidateIds = selectSemanticCandidateIds(query, candidates);
|
|
2157
2157
|
const semanticBonuses = semanticEnabled
|
|
2158
|
-
? buildSemanticCandidateBonusMap(await repository.rankSemanticCandidates(query,
|
|
2158
|
+
? buildSemanticCandidateBonusMap(await repository.rankSemanticCandidates(query, semanticCandidateIds), semanticAugmentation)
|
|
2159
2159
|
: new Map();
|
|
2160
2160
|
const result = candidates
|
|
2161
2161
|
.map((node) => {
|
|
@@ -2179,7 +2179,9 @@ export function createRecallXApp(params) {
|
|
|
2179
2179
|
.sort((left, right) => right.score - left.score);
|
|
2180
2180
|
span.addDetails({
|
|
2181
2181
|
resultCount: result.length,
|
|
2182
|
-
semanticUsed: semanticEnabled
|
|
2182
|
+
semanticUsed: semanticEnabled,
|
|
2183
|
+
semanticCandidateCount: candidates.length,
|
|
2184
|
+
semanticRankedCandidateCount: semanticCandidateIds.length
|
|
2183
2185
|
});
|
|
2184
2186
|
return result;
|
|
2185
2187
|
});
|
|
@@ -14,6 +14,9 @@ function normalizeText(value) {
|
|
|
14
14
|
function normalizeTags(tags) {
|
|
15
15
|
return Array.from(new Set(tags.map((tag) => normalizeText(tag)).filter(Boolean)));
|
|
16
16
|
}
|
|
17
|
+
function normalizeTexts(values) {
|
|
18
|
+
return values.map(normalizeText).filter(Boolean);
|
|
19
|
+
}
|
|
17
20
|
function escapeRegExp(value) {
|
|
18
21
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
22
|
}
|
|
@@ -33,6 +36,10 @@ function mentionsNode(haystacks, candidate) {
|
|
|
33
36
|
: false;
|
|
34
37
|
return { idMention, titleMention };
|
|
35
38
|
}
|
|
39
|
+
function hasCheapMention(normalizedHaystacks, candidate) {
|
|
40
|
+
const normalizedTitle = normalizeText(candidate.title);
|
|
41
|
+
return normalizedHaystacks.some((haystack) => haystack.includes(candidate.id.toLowerCase()) || (normalizedTitle.length >= 5 && haystack.includes(normalizedTitle)));
|
|
42
|
+
}
|
|
36
43
|
function sortPair(left, right) {
|
|
37
44
|
return left.localeCompare(right) <= 0 ? [left, right] : [right, left];
|
|
38
45
|
}
|
|
@@ -154,36 +161,42 @@ function collectGeneratedCandidates(repository, target, trigger) {
|
|
|
154
161
|
projectIds: new Set(projectMembershipsByNodeId.get(target.id) ?? []),
|
|
155
162
|
artifactKeys: artifactKeysByNodeId.get(target.id) ?? { exactPaths: [], baseNames: [] }
|
|
156
163
|
};
|
|
164
|
+
const normalizedTargetTexts = normalizeTexts([target.title, target.body, target.summary]);
|
|
157
165
|
const activityBodies = trigger === "activity-append" || trigger === "reindex"
|
|
158
166
|
? repository
|
|
159
167
|
.listNodeActivities(target.id, MAX_ACTIVITY_BODIES)
|
|
160
168
|
.map((activity) => activity.body ?? "")
|
|
161
169
|
.filter(Boolean)
|
|
162
170
|
: [];
|
|
171
|
+
const normalizedActivityBodies = activityBodies.map(normalizeText).filter(Boolean);
|
|
163
172
|
return candidates.flatMap((candidate) => {
|
|
164
173
|
const generated = [];
|
|
174
|
+
const candidateProjectIds = projectMembershipsByNodeId.get(candidate.id) ?? [];
|
|
175
|
+
const candidateArtifacts = artifactKeysByNodeId.get(candidate.id) ?? { exactPaths: [], baseNames: [] };
|
|
165
176
|
const tagOverlapCandidate = buildTagOverlapCandidate(target, candidate, targetContext.normalizedTags);
|
|
166
177
|
if (tagOverlapCandidate) {
|
|
167
178
|
generated.push(tagOverlapCandidate);
|
|
168
179
|
}
|
|
169
|
-
const
|
|
180
|
+
const projectMembershipCandidate = buildProjectMembershipCandidate(target, candidate, candidateProjectIds, targetContext.projectIds);
|
|
181
|
+
if (projectMembershipCandidate) {
|
|
182
|
+
generated.push(projectMembershipCandidate);
|
|
183
|
+
}
|
|
184
|
+
const sharedArtifactCandidate = buildSharedArtifactCandidate(target, candidate, candidateArtifacts, targetContext.artifactKeys);
|
|
185
|
+
if (sharedArtifactCandidate) {
|
|
186
|
+
generated.push(sharedArtifactCandidate);
|
|
187
|
+
}
|
|
188
|
+
const candidateTexts = normalizeTexts([candidate.title, candidate.body, candidate.summary]);
|
|
189
|
+
const shouldCheckBodyReference = hasCheapMention(normalizedTargetTexts, candidate) || hasCheapMention(candidateTexts, target);
|
|
190
|
+
const bodyReferenceCandidate = shouldCheckBodyReference ? buildBodyReferenceCandidate(target, candidate) : null;
|
|
170
191
|
if (bodyReferenceCandidate) {
|
|
171
192
|
generated.push(bodyReferenceCandidate);
|
|
172
193
|
}
|
|
173
|
-
if (
|
|
194
|
+
if (normalizedActivityBodies.length && hasCheapMention(normalizedActivityBodies, candidate)) {
|
|
174
195
|
const activityReferenceCandidate = buildActivityReferenceCandidate(target, candidate, activityBodies);
|
|
175
196
|
if (activityReferenceCandidate) {
|
|
176
197
|
generated.push(activityReferenceCandidate);
|
|
177
198
|
}
|
|
178
199
|
}
|
|
179
|
-
const projectMembershipCandidate = buildProjectMembershipCandidate(target, candidate, projectMembershipsByNodeId.get(candidate.id) ?? [], targetContext.projectIds);
|
|
180
|
-
if (projectMembershipCandidate) {
|
|
181
|
-
generated.push(projectMembershipCandidate);
|
|
182
|
-
}
|
|
183
|
-
const sharedArtifactCandidate = buildSharedArtifactCandidate(target, candidate, artifactKeysByNodeId.get(candidate.id) ?? { exactPaths: [], baseNames: [] }, targetContext.artifactKeys);
|
|
184
|
-
if (sharedArtifactCandidate) {
|
|
185
|
-
generated.push(sharedArtifactCandidate);
|
|
186
|
-
}
|
|
187
200
|
return generated;
|
|
188
201
|
});
|
|
189
202
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { appendFile, mkdir, readdir, unlink } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
4
6
|
import { createId } from "./utils.js";
|
|
5
7
|
const telemetryStorage = new AsyncLocalStorage();
|
|
6
8
|
function nowIso() {
|
|
@@ -45,6 +47,10 @@ function roundDuration(value) {
|
|
|
45
47
|
function dateStamp(value) {
|
|
46
48
|
return value.slice(0, 10);
|
|
47
49
|
}
|
|
50
|
+
function telemetryLogDate(entry) {
|
|
51
|
+
const match = /^telemetry-(\d{4}-\d{2}-\d{2})\.ndjson$/.exec(entry);
|
|
52
|
+
return match ? match[1] : null;
|
|
53
|
+
}
|
|
48
54
|
function normalizeRetentionDays(value) {
|
|
49
55
|
return Math.max(1, Math.trunc(value || 14));
|
|
50
56
|
}
|
|
@@ -341,24 +347,41 @@ export class ObservabilityWriter {
|
|
|
341
347
|
}
|
|
342
348
|
const files = entries
|
|
343
349
|
.filter((entry) => /^telemetry-\d{4}-\d{2}-\d{2}\.ndjson$/.test(entry))
|
|
350
|
+
.filter((entry) => {
|
|
351
|
+
const stamp = telemetryLogDate(entry);
|
|
352
|
+
return stamp !== null && stamp >= dateStamp(new Date(sinceMs).toISOString());
|
|
353
|
+
})
|
|
344
354
|
.sort();
|
|
345
355
|
const events = [];
|
|
346
356
|
for (const file of files) {
|
|
347
357
|
const filePath = path.join(logsDir, file);
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
359
|
+
const lines = createInterface({
|
|
360
|
+
input: stream,
|
|
361
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
for await (const line of lines) {
|
|
365
|
+
const event = parseJsonLine(line);
|
|
366
|
+
if (!event) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const eventMs = Date.parse(event.ts);
|
|
370
|
+
if (!Number.isFinite(eventMs) || eventMs < sinceMs) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (options.surface && options.surface !== "all" && event.surface !== options.surface) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
events.push(event);
|
|
360
377
|
}
|
|
361
|
-
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Ignore individual file read failures so observability endpoints stay resilient.
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
lines.close();
|
|
384
|
+
stream.destroy();
|
|
362
385
|
}
|
|
363
386
|
}
|
|
364
387
|
return {
|
|
@@ -724,7 +747,9 @@ export class ObservabilityWriter {
|
|
|
724
747
|
generatedAt: nowIso(),
|
|
725
748
|
logsPath,
|
|
726
749
|
totalEvents: events.length,
|
|
750
|
+
slowRequestThresholdMs: state.slowRequestMs,
|
|
727
751
|
operationSummaries,
|
|
752
|
+
hotOperations: operationSummaries.slice(0, 10),
|
|
728
753
|
slowOperations: operationSummaries
|
|
729
754
|
.filter((item) => (item.p95DurationMs ?? 0) >= state.slowRequestMs)
|
|
730
755
|
.slice(0, 10),
|
|
@@ -59,12 +59,14 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
59
59
|
matchedFields: [],
|
|
60
60
|
exactFields: [],
|
|
61
61
|
matchedTermCount: 0,
|
|
62
|
+
matchedTermCounts: {},
|
|
62
63
|
totalTermCount: 0
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
const matchedFields = new Set();
|
|
66
67
|
const exactFields = new Set();
|
|
67
68
|
const matchedTerms = new Set();
|
|
69
|
+
const matchedTermCounts = {};
|
|
68
70
|
for (const candidate of candidates) {
|
|
69
71
|
const haystack = normalizeSearchText(candidate.value);
|
|
70
72
|
if (!haystack) {
|
|
@@ -76,6 +78,7 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
78
80
|
matchedFields.add(candidate.field);
|
|
81
|
+
matchedTermCounts[candidate.field] = termMatches.length;
|
|
79
82
|
if (exactMatch) {
|
|
80
83
|
exactFields.add(candidate.field);
|
|
81
84
|
}
|
|
@@ -87,6 +90,7 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
87
90
|
matchedFields: [...matchedFields],
|
|
88
91
|
exactFields: [...exactFields],
|
|
89
92
|
matchedTermCount: matchedTerms.size,
|
|
93
|
+
matchedTermCounts,
|
|
90
94
|
totalTermCount: matcher.matchTerms.length
|
|
91
95
|
};
|
|
92
96
|
}
|
|
@@ -94,14 +98,18 @@ function classifyNodeLexicalQuality(strategy, signals) {
|
|
|
94
98
|
if (strategy === "browse" || strategy === "semantic" || !signals.matchedFields.length) {
|
|
95
99
|
return "none";
|
|
96
100
|
}
|
|
97
|
-
|
|
98
|
-
return "weak";
|
|
99
|
-
}
|
|
100
|
-
const strongExactFields = new Set(["title", "summary", "tags"]);
|
|
101
|
+
const strongExactFields = new Set(["title", "summary", "tags", "body"]);
|
|
101
102
|
if (signals.exactFields.some((field) => strongExactFields.has(field))) {
|
|
102
103
|
return "strong";
|
|
103
104
|
}
|
|
104
105
|
const termCoverage = signals.totalTermCount > 0 ? signals.matchedTermCount / signals.totalTermCount : 0;
|
|
106
|
+
const titleCoverage = signals.totalTermCount > 0 ? (signals.matchedTermCounts.title ?? 0) / signals.totalTermCount : 0;
|
|
107
|
+
if (strategy === "fallback_token") {
|
|
108
|
+
return titleCoverage >= 0.5 ? "strong" : "weak";
|
|
109
|
+
}
|
|
110
|
+
if (strategy === "fts" && titleCoverage >= 0.5) {
|
|
111
|
+
return "strong";
|
|
112
|
+
}
|
|
105
113
|
if (strategy === "fts" && termCoverage >= 0.6 && signals.matchedFields.some((field) => strongExactFields.has(field))) {
|
|
106
114
|
return "strong";
|
|
107
115
|
}
|
|
@@ -2312,25 +2320,59 @@ export class RecallXRepository {
|
|
|
2312
2320
|
: "";
|
|
2313
2321
|
const rows = this.db
|
|
2314
2322
|
.prepare(`SELECT
|
|
2315
|
-
r
|
|
2316
|
-
|
|
2323
|
+
r.id,
|
|
2324
|
+
r.from_node_id,
|
|
2325
|
+
r.to_node_id,
|
|
2326
|
+
r.relation_type,
|
|
2327
|
+
r.status,
|
|
2328
|
+
r.created_by,
|
|
2329
|
+
r.source_type,
|
|
2330
|
+
r.source_label,
|
|
2331
|
+
r.created_at,
|
|
2332
|
+
r.metadata_json,
|
|
2333
|
+
n.id AS node_id,
|
|
2334
|
+
n.type AS node_type,
|
|
2335
|
+
n.status AS node_status,
|
|
2336
|
+
n.canonicality AS node_canonicality,
|
|
2337
|
+
n.visibility AS node_visibility,
|
|
2338
|
+
n.title AS node_title,
|
|
2339
|
+
n.body AS node_body,
|
|
2340
|
+
n.summary AS node_summary,
|
|
2341
|
+
n.created_by AS node_created_by,
|
|
2342
|
+
n.source_type AS node_source_type,
|
|
2343
|
+
n.source_label AS node_source_label,
|
|
2344
|
+
n.created_at AS node_created_at,
|
|
2345
|
+
n.updated_at AS node_updated_at,
|
|
2346
|
+
n.tags_json AS node_tags_json,
|
|
2347
|
+
n.metadata_json AS node_metadata_json
|
|
2317
2348
|
FROM relations r
|
|
2349
|
+
JOIN nodes n
|
|
2350
|
+
ON n.id = CASE WHEN r.from_node_id = ? THEN r.to_node_id ELSE r.from_node_id END
|
|
2318
2351
|
WHERE (r.from_node_id = ? OR r.to_node_id = ?)
|
|
2319
2352
|
AND r.status != 'archived'
|
|
2320
2353
|
${relationWhere}
|
|
2321
2354
|
ORDER BY r.created_at DESC`)
|
|
2322
2355
|
.all(nodeId, nodeId, nodeId, ...(relationFilter ?? []));
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2356
|
+
return rows.map((row) => ({
|
|
2357
|
+
relation: mapRelation(row),
|
|
2358
|
+
node: {
|
|
2359
|
+
id: String(row.node_id),
|
|
2360
|
+
type: row.node_type,
|
|
2361
|
+
status: row.node_status,
|
|
2362
|
+
canonicality: row.node_canonicality,
|
|
2363
|
+
visibility: String(row.node_visibility),
|
|
2364
|
+
title: row.node_title ? String(row.node_title) : null,
|
|
2365
|
+
body: row.node_body ? String(row.node_body) : null,
|
|
2366
|
+
summary: row.node_summary ? String(row.node_summary) : null,
|
|
2367
|
+
createdBy: row.node_created_by ? String(row.node_created_by) : null,
|
|
2368
|
+
sourceType: row.node_source_type ? String(row.node_source_type) : null,
|
|
2369
|
+
sourceLabel: row.node_source_label ? String(row.node_source_label) : null,
|
|
2370
|
+
createdAt: String(row.node_created_at),
|
|
2371
|
+
updatedAt: String(row.node_updated_at),
|
|
2372
|
+
tags: parseJson(row.node_tags_json, []),
|
|
2373
|
+
metadata: parseJson(row.node_metadata_json, {})
|
|
2328
2374
|
}
|
|
2329
|
-
|
|
2330
|
-
relation: mapRelation(row),
|
|
2331
|
-
node
|
|
2332
|
-
}];
|
|
2333
|
-
});
|
|
2375
|
+
}));
|
|
2334
2376
|
}
|
|
2335
2377
|
listProjectMemberNodes(projectId, limit) {
|
|
2336
2378
|
const rows = this.db
|
package/app/server/retrieval.js
CHANGED
|
@@ -18,6 +18,7 @@ const boostedRelationRankWeights = {
|
|
|
18
18
|
};
|
|
19
19
|
const semanticCandidateMinSimilarity = 0.2;
|
|
20
20
|
const semanticCandidateMaxBonus = 18;
|
|
21
|
+
const maxSemanticPrefilterCandidates = 256;
|
|
21
22
|
function resolveSemanticAugmentationSettings(settings) {
|
|
22
23
|
return {
|
|
23
24
|
minSimilarity: typeof settings?.minSimilarity === "number" && Number.isFinite(settings.minSimilarity)
|
|
@@ -38,7 +39,15 @@ function prioritizeItems(items, preset, maxItems, bonuses) {
|
|
|
38
39
|
.map(({ item }) => item);
|
|
39
40
|
return weighted.slice(0, maxItems);
|
|
40
41
|
}
|
|
41
|
-
function
|
|
42
|
+
function normalizeRetrievalText(value) {
|
|
43
|
+
return (value ?? "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
44
|
+
}
|
|
45
|
+
function tokenizeRetrievalText(value) {
|
|
46
|
+
return normalizeRetrievalText(value)
|
|
47
|
+
.split(/[^a-z0-9]+/g)
|
|
48
|
+
.filter((token) => token.length >= 3);
|
|
49
|
+
}
|
|
50
|
+
function listNeighborhoodCandidates(repository, nodeId, options) {
|
|
42
51
|
const canonicalItems = repository.listRelatedNodes(nodeId, 1, options?.relationTypes).map(({ node, relation }) => ({
|
|
43
52
|
node,
|
|
44
53
|
edge: {
|
|
@@ -94,6 +103,13 @@ function buildNeighborhoodResult(repository, nodeId, options) {
|
|
|
94
103
|
});
|
|
95
104
|
})()
|
|
96
105
|
: [];
|
|
106
|
+
return {
|
|
107
|
+
canonicalItems,
|
|
108
|
+
inferredItems
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function buildNeighborhoodResult(repository, nodeId, options) {
|
|
112
|
+
const { canonicalItems, inferredItems } = listNeighborhoodCandidates(repository, nodeId, options);
|
|
97
113
|
const usageSummaries = repository.getRelationUsageSummaries([...canonicalItems, ...inferredItems].map((item) => item.edge.relationId));
|
|
98
114
|
const rankedCanonical = rankNeighborhoodItems(canonicalItems, usageSummaries, neighborhoodRetrievalRankWeights);
|
|
99
115
|
const rankedInferred = options?.includeInferred && options.maxInferred
|
|
@@ -104,6 +120,27 @@ function buildNeighborhoodResult(repository, nodeId, options) {
|
|
|
104
120
|
usageSummaries
|
|
105
121
|
};
|
|
106
122
|
}
|
|
123
|
+
export function buildNeighborhoodItemsBatch(repository, nodeIds, options) {
|
|
124
|
+
const uniqueNodeIds = Array.from(new Set(nodeIds.filter(Boolean)));
|
|
125
|
+
const rawByNodeId = new Map();
|
|
126
|
+
const relationIds = [];
|
|
127
|
+
for (const nodeId of uniqueNodeIds) {
|
|
128
|
+
const raw = listNeighborhoodCandidates(repository, nodeId, options);
|
|
129
|
+
rawByNodeId.set(nodeId, raw);
|
|
130
|
+
relationIds.push(...raw.canonicalItems.map((item) => item.edge.relationId));
|
|
131
|
+
relationIds.push(...raw.inferredItems.map((item) => item.edge.relationId));
|
|
132
|
+
}
|
|
133
|
+
const usageSummaries = repository.getRelationUsageSummaries(relationIds);
|
|
134
|
+
const results = new Map();
|
|
135
|
+
for (const [nodeId, raw] of rawByNodeId.entries()) {
|
|
136
|
+
const rankedCanonical = rankNeighborhoodItems(raw.canonicalItems, usageSummaries, neighborhoodRetrievalRankWeights);
|
|
137
|
+
const rankedInferred = options?.includeInferred && options.maxInferred
|
|
138
|
+
? rankNeighborhoodItems(raw.inferredItems, usageSummaries, neighborhoodRetrievalRankWeights, options.maxInferred)
|
|
139
|
+
: [];
|
|
140
|
+
results.set(nodeId, [...rankedCanonical, ...rankedInferred]);
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
107
144
|
function matchesSearchResultFilters(item, filters) {
|
|
108
145
|
const typeMatches = !filters.types?.length || filters.types.includes(item.type);
|
|
109
146
|
const statusMatches = !filters.status?.length || filters.status.includes(item.status);
|
|
@@ -214,6 +251,45 @@ export function buildSemanticCandidateBonusMap(semanticMatches, settings) {
|
|
|
214
251
|
];
|
|
215
252
|
}));
|
|
216
253
|
}
|
|
254
|
+
function scoreSemanticPrefilterCandidate(normalizedQuery, queryTokens, candidate) {
|
|
255
|
+
const normalizedTitle = normalizeRetrievalText(candidate.title);
|
|
256
|
+
const normalizedSummary = normalizeRetrievalText(candidate.summary);
|
|
257
|
+
let score = 0;
|
|
258
|
+
if (normalizedTitle.includes(normalizedQuery)) {
|
|
259
|
+
score += 12;
|
|
260
|
+
}
|
|
261
|
+
if (normalizedSummary.includes(normalizedQuery)) {
|
|
262
|
+
score += 6;
|
|
263
|
+
}
|
|
264
|
+
for (const token of queryTokens) {
|
|
265
|
+
if (normalizedTitle.includes(token)) {
|
|
266
|
+
score += 2;
|
|
267
|
+
}
|
|
268
|
+
if (normalizedSummary.includes(token)) {
|
|
269
|
+
score += 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return score;
|
|
273
|
+
}
|
|
274
|
+
export function selectSemanticCandidateIds(query, candidates, maxCandidates = maxSemanticPrefilterCandidates) {
|
|
275
|
+
if (candidates.length <= maxCandidates) {
|
|
276
|
+
return candidates.map((candidate) => candidate.id);
|
|
277
|
+
}
|
|
278
|
+
const normalizedQuery = normalizeRetrievalText(query);
|
|
279
|
+
if (!normalizedQuery) {
|
|
280
|
+
return candidates.slice(0, maxCandidates).map((candidate) => candidate.id);
|
|
281
|
+
}
|
|
282
|
+
const queryTokens = Array.from(new Set(tokenizeRetrievalText(query)));
|
|
283
|
+
return candidates
|
|
284
|
+
.map((candidate) => ({
|
|
285
|
+
id: candidate.id,
|
|
286
|
+
score: scoreSemanticPrefilterCandidate(normalizedQuery, queryTokens, candidate),
|
|
287
|
+
updatedAt: candidate.updatedAt
|
|
288
|
+
}))
|
|
289
|
+
.sort((left, right) => right.score - left.score || right.updatedAt.localeCompare(left.updatedAt))
|
|
290
|
+
.slice(0, maxCandidates)
|
|
291
|
+
.map((candidate) => candidate.id);
|
|
292
|
+
}
|
|
217
293
|
function computeBundleRelationBoost(item, summary) {
|
|
218
294
|
return computeRelationRetrievalRank(item.edge, summary, {
|
|
219
295
|
canonicalBase: 120,
|
|
@@ -397,15 +473,19 @@ export async function buildContextBundle(repository, input) {
|
|
|
397
473
|
const candidateItems = [targetItem, ...relatedItems, ...decisions, ...openQuestions];
|
|
398
474
|
const dedupedItems = Array.from(new Map(candidateItems.map((item) => [item.id, item])).values());
|
|
399
475
|
const semanticQuery = [target.title, target.summary ?? target.body].filter(Boolean).join("\n");
|
|
400
|
-
const
|
|
401
|
-
|
|
476
|
+
const semanticCandidates = dedupedItems.filter((item) => item.id !== target.id);
|
|
477
|
+
const semanticCandidateIds = selectSemanticCandidateIds(semanticQuery, semanticCandidates);
|
|
478
|
+
const semanticBonuses = shouldUseSemanticCandidateAugmentation(semanticQuery, semanticCandidates)
|
|
479
|
+
? buildSemanticCandidateBonusMap(await repository.rankSemanticCandidates(semanticQuery, semanticCandidateIds), repository.getSemanticAugmentationSettings())
|
|
402
480
|
: new Map();
|
|
403
481
|
appendCurrentTelemetryDetails({
|
|
404
482
|
neighborhoodCount: neighborhood.length,
|
|
405
483
|
relatedCandidateCount: relatedItems.length,
|
|
406
484
|
decisionCount: decisions.length,
|
|
407
485
|
openQuestionCount: openQuestions.length,
|
|
408
|
-
semanticUsed: semanticBonuses.size > 0
|
|
486
|
+
semanticUsed: semanticBonuses.size > 0,
|
|
487
|
+
semanticCandidateCount: semanticCandidates.length,
|
|
488
|
+
semanticRankedCandidateCount: semanticCandidateIds.length
|
|
409
489
|
});
|
|
410
490
|
const combinedBonuses = new Map();
|
|
411
491
|
for (const item of dedupedItems) {
|
|
@@ -98,7 +98,7 @@ export class WorkspaceSessionManager {
|
|
|
98
98
|
"relations.autoRecompute.lastRunAt": null,
|
|
99
99
|
"observability.enabled": false,
|
|
100
100
|
"observability.retentionDays": 14,
|
|
101
|
-
"observability.slowRequestMs":
|
|
101
|
+
"observability.slowRequestMs": 50,
|
|
102
102
|
"observability.capturePayloadShape": true,
|
|
103
103
|
"export.defaultFormat": "markdown",
|
|
104
104
|
});
|
package/app/shared/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RECALLX_VERSION = "1.0
|
|
1
|
+
export const RECALLX_VERSION = "1.1.0";
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recallx-headless",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Headless RecallX runtime with API, CLI, and MCP entrypoint.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "jazpiper",
|
|
8
|
+
"email": "jazpiper1@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
6
11
|
"bin": {
|
|
7
12
|
"recallx": "./app/cli/bin/recallx.js",
|
|
8
13
|
"recallx-mcp": "./app/cli/bin/recallx-mcp.js"
|
|
@@ -12,7 +17,8 @@
|
|
|
12
17
|
"app/mcp",
|
|
13
18
|
"app/shared",
|
|
14
19
|
"app/server",
|
|
15
|
-
"README.md"
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
16
22
|
],
|
|
17
23
|
"dependencies": {
|
|
18
24
|
"@modelcontextprotocol/sdk": "^1.27.1",
|