recallx 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 +23 -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/dist/renderer/assets/{ProjectGraphCanvas-YhLhWpYR.js → ProjectGraphCanvas-WP0YEOpB.js} +1 -1
- package/dist/renderer/assets/{index-Bwm9VwHm.js → index-5rwy6MBF.js} +2 -2
- package/dist/renderer/index.html +1 -1
- package/package.json +8 -2
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";
|