recallx-headless 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/app/cli/src/cli.js +8 -1
- package/app/cli/src/format.js +54 -0
- package/app/cli/src/http.js +1 -1
- package/app/server/app.js +65 -3
- package/app/server/observability.js +381 -1
- package/app/server/repositories.js +241 -41
- package/app/server/workspace-session.js +1 -0
- package/app/shared/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -132,9 +132,10 @@ recallx workspace open --root /Users/name/Documents/RecallX-Test
|
|
|
132
132
|
## Environment
|
|
133
133
|
|
|
134
134
|
- `RECALLX_API_URL` to override the local API base URL
|
|
135
|
-
- `
|
|
135
|
+
- `RECALLX_API_TOKEN` to pass a bearer token for CLI requests
|
|
136
|
+
- `RECALLX_TOKEN` remains supported as a legacy alias for CLI requests
|
|
136
137
|
- `RECALLX_PORT`, `RECALLX_BIND`, `RECALLX_WORKSPACE_ROOT`, `RECALLX_WORKSPACE_NAME`, and `RECALLX_API_TOKEN` are respected by `recallx serve`
|
|
137
|
-
- Node
|
|
138
|
+
- Node 22.13+ is recommended for the headless package
|
|
138
139
|
|
|
139
140
|
## Notes
|
|
140
141
|
|
package/app/cli/src/cli.js
CHANGED
|
@@ -726,7 +726,14 @@ function buildMcpConfigPayload(launcherPath) {
|
|
|
726
726
|
}
|
|
727
727
|
async function installMcpLauncher(launcherPath, commandParts) {
|
|
728
728
|
await mkdir(path.dirname(launcherPath), { recursive: true });
|
|
729
|
-
await writeFile(launcherPath, `#!/bin/sh
|
|
729
|
+
await writeFile(launcherPath, `#!/bin/sh
|
|
730
|
+
NODE_BIN="\${RECALLX_NODE_BIN:-$(command -v node 2>/dev/null || true)}"
|
|
731
|
+
if [ -z "$NODE_BIN" ]; then
|
|
732
|
+
echo "recallx-mcp launcher could not find a node executable in PATH." >&2
|
|
733
|
+
exit 1
|
|
734
|
+
fi
|
|
735
|
+
exec "$NODE_BIN" ${commandParts.slice(1).map(quoteShellArg).join(" ")} "$@"
|
|
736
|
+
`, "utf8");
|
|
730
737
|
await chmod(launcherPath, 0o755);
|
|
731
738
|
}
|
|
732
739
|
function quoteShellArg(value) {
|
package/app/cli/src/format.js
CHANGED
|
@@ -202,11 +202,65 @@ export function renderTelemetrySummary(data) {
|
|
|
202
202
|
`fts fallback: ${data.ftsFallbackRate.fallbackCount}/${data.ftsFallbackRate.sampleCount} (${data.ftsFallbackRate.ratio ?? "n/a"})`
|
|
203
203
|
);
|
|
204
204
|
}
|
|
205
|
+
if (data?.searchHitRate) {
|
|
206
|
+
lines.push(
|
|
207
|
+
`search hit rate: ${data.searchHitRate.hitCount}/${data.searchHitRate.sampleCount} (${data.searchHitRate.ratio ?? "n/a"})`
|
|
208
|
+
);
|
|
209
|
+
const searchHitOps = Array.isArray(data.searchHitRate.operations) ? data.searchHitRate.operations : [];
|
|
210
|
+
for (const item of searchHitOps.slice(0, 5)) {
|
|
211
|
+
lines.push(
|
|
212
|
+
`- [${item.surface}] ${item.operation}: ${item.hitCount}/${item.sampleCount} hits (${item.ratio ?? "n/a"})`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (data?.searchLexicalQualityRate) {
|
|
217
|
+
lines.push(
|
|
218
|
+
`lexical quality: strong=${data.searchLexicalQualityRate.strongCount}, weak=${data.searchLexicalQualityRate.weakCount}, none=${data.searchLexicalQualityRate.noneCount}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (data?.workspaceResultCompositionRate) {
|
|
222
|
+
lines.push(
|
|
223
|
+
`workspace composition: node_only=${data.workspaceResultCompositionRate.nodeOnlyCount}, semantic_node_only=${data.workspaceResultCompositionRate.semanticNodeOnlyCount}, activity_only=${data.workspaceResultCompositionRate.activityOnlyCount}, mixed=${data.workspaceResultCompositionRate.mixedCount}, semantic_mixed=${data.workspaceResultCompositionRate.semanticMixedCount}, empty=${data.workspaceResultCompositionRate.emptyCount}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (data?.workspaceFallbackModeRate) {
|
|
227
|
+
lines.push(
|
|
228
|
+
`workspace fallback modes: strict_zero=${data.workspaceFallbackModeRate.strictZeroCount}, no_strong_node_hit=${data.workspaceFallbackModeRate.noStrongNodeHitCount}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (data?.searchFeedbackRate) {
|
|
232
|
+
lines.push(
|
|
233
|
+
`search feedback: useful=${data.searchFeedbackRate.usefulCount}/${data.searchFeedbackRate.sampleCount} (${data.searchFeedbackRate.usefulRatio ?? "n/a"}), top1=${data.searchFeedbackRate.top1UsefulCount}/${data.searchFeedbackRate.top1SampleCount} (${data.searchFeedbackRate.top1UsefulRatio ?? "n/a"}), top3=${data.searchFeedbackRate.top3UsefulCount}/${data.searchFeedbackRate.top3SampleCount} (${data.searchFeedbackRate.top3UsefulRatio ?? "n/a"}), semantic_fp=${data.searchFeedbackRate.semanticFalsePositiveRatio ?? "n/a"}`
|
|
234
|
+
);
|
|
235
|
+
const qualityBuckets = Array.isArray(data.searchFeedbackRate.byLexicalQuality) ? data.searchFeedbackRate.byLexicalQuality : [];
|
|
236
|
+
for (const item of qualityBuckets) {
|
|
237
|
+
lines.push(
|
|
238
|
+
`- feedback [${item.lexicalQuality}]: ${item.usefulCount}/${item.sampleCount} useful (${item.usefulRatio ?? "n/a"})`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const fallbackModeBuckets = Array.isArray(data.searchFeedbackRate.byFallbackMode) ? data.searchFeedbackRate.byFallbackMode : [];
|
|
242
|
+
for (const item of fallbackModeBuckets) {
|
|
243
|
+
lines.push(
|
|
244
|
+
`- feedback mode [${item.fallbackMode}]: ${item.usefulCount}/${item.sampleCount} useful (${item.usefulRatio ?? "n/a"})`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
205
248
|
if (data?.semanticAugmentationRate) {
|
|
206
249
|
lines.push(
|
|
207
250
|
`semantic augmentation: ${data.semanticAugmentationRate.usedCount}/${data.semanticAugmentationRate.sampleCount} (${data.semanticAugmentationRate.ratio ?? "n/a"})`
|
|
208
251
|
);
|
|
209
252
|
}
|
|
253
|
+
if (data?.semanticFallbackRate) {
|
|
254
|
+
lines.push(
|
|
255
|
+
`semantic fallback: eligible=${data.semanticFallbackRate.eligibleCount}, attempted=${data.semanticFallbackRate.attemptedCount}, hit=${data.semanticFallbackRate.hitCount}, hit_ratio=${data.semanticFallbackRate.hitRatio ?? "n/a"}`
|
|
256
|
+
);
|
|
257
|
+
const fallbackModes = Array.isArray(data.semanticFallbackRate.modes) ? data.semanticFallbackRate.modes : [];
|
|
258
|
+
for (const item of fallbackModes) {
|
|
259
|
+
lines.push(
|
|
260
|
+
`- fallback mode [${item.fallbackMode}]: attempted=${item.attemptedCount}/${item.eligibleCount} (${item.attemptRatio ?? "n/a"}), hit=${item.hitCount}/${item.attemptedCount} (${item.hitRatio ?? "n/a"})`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
210
264
|
|
|
211
265
|
return `${lines.join("\n")}\n`;
|
|
212
266
|
}
|
package/app/cli/src/http.js
CHANGED
|
@@ -6,7 +6,7 @@ export function getApiBase(argvOptions = {}, env = process.env) {
|
|
|
6
6
|
DEFAULT_API_BASE);
|
|
7
7
|
}
|
|
8
8
|
export function getAuthToken(argvOptions = {}, env = process.env) {
|
|
9
|
-
return argvOptions.token || env.RECALLX_TOKEN || "";
|
|
9
|
+
return argvOptions.token || env.RECALLX_API_TOKEN || env.RECALLX_TOKEN || "";
|
|
10
10
|
}
|
|
11
11
|
export async function requestJson(apiBase, path, { method = "GET", token, body } = {}) {
|
|
12
12
|
const response = await fetch(buildApiUrl(apiBase, path), buildApiRequestInit({ method, token, body }));
|
package/app/server/app.js
CHANGED
|
@@ -8,7 +8,7 @@ import { activitySearchSchema, appendActivitySchema, appendRelationUsageEventSch
|
|
|
8
8
|
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
|
-
import { createObservabilityWriter, summarizePayloadShape } from "./observability.js";
|
|
11
|
+
import { appendCurrentTelemetryDetails, createObservabilityWriter, summarizePayloadShape } from "./observability.js";
|
|
12
12
|
import { buildSemanticCandidateBonusMap, buildCandidateRelationBonusMap, buildContextBundle, buildNeighborhoodItems, buildTargetRelatedRetrievalItems, bundleAsMarkdown, computeRankCandidateScore, shouldUseSemanticCandidateAugmentation } from "./retrieval.js";
|
|
13
13
|
import { buildProjectGraph } from "./project-graph.js";
|
|
14
14
|
import { createId, isPathWithinRoot } from "./utils.js";
|
|
@@ -172,6 +172,9 @@ function parseBooleanSetting(value, fallback) {
|
|
|
172
172
|
function parseNumberSetting(value, fallback) {
|
|
173
173
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
174
174
|
}
|
|
175
|
+
function readWorkspaceSemanticFallbackMode(value) {
|
|
176
|
+
return value === "strict_zero" || value === "no_strong_node_hit" ? value : null;
|
|
177
|
+
}
|
|
175
178
|
function normalizeApiRequestPath(value) {
|
|
176
179
|
return value
|
|
177
180
|
.replace(/^\/artifacts\/.+$/g, "/artifacts/:path")
|
|
@@ -1406,6 +1409,12 @@ export function createRecallXApp(params) {
|
|
|
1406
1409
|
}, (span) => {
|
|
1407
1410
|
const searchResult = currentRepository().searchNodes(input);
|
|
1408
1411
|
span.addDetails({
|
|
1412
|
+
searchHit: searchResult.items.length > 0,
|
|
1413
|
+
bestLexicalQuality: searchResult.items.some((item) => item.lexicalQuality === "strong")
|
|
1414
|
+
? "strong"
|
|
1415
|
+
: searchResult.items.some((item) => item.lexicalQuality === "weak")
|
|
1416
|
+
? "weak"
|
|
1417
|
+
: "none",
|
|
1409
1418
|
resultCount: searchResult.items.length,
|
|
1410
1419
|
totalCount: searchResult.total
|
|
1411
1420
|
});
|
|
@@ -1423,6 +1432,12 @@ export function createRecallXApp(params) {
|
|
|
1423
1432
|
}, (span) => {
|
|
1424
1433
|
const searchResult = currentRepository().searchActivities(input);
|
|
1425
1434
|
span.addDetails({
|
|
1435
|
+
searchHit: searchResult.items.length > 0,
|
|
1436
|
+
bestLexicalQuality: searchResult.items.some((item) => item.lexicalQuality === "strong")
|
|
1437
|
+
? "strong"
|
|
1438
|
+
: searchResult.items.some((item) => item.lexicalQuality === "weak")
|
|
1439
|
+
? "weak"
|
|
1440
|
+
: "none",
|
|
1426
1441
|
resultCount: searchResult.items.length,
|
|
1427
1442
|
totalCount: searchResult.total
|
|
1428
1443
|
});
|
|
@@ -1457,6 +1472,7 @@ export function createRecallXApp(params) {
|
|
|
1457
1472
|
})
|
|
1458
1473
|
});
|
|
1459
1474
|
span.addDetails({
|
|
1475
|
+
searchHit: searchResult.items.length > 0,
|
|
1460
1476
|
resultCount: searchResult.items.length,
|
|
1461
1477
|
totalCount: searchResult.total
|
|
1462
1478
|
});
|
|
@@ -1861,9 +1877,55 @@ export function createRecallXApp(params) {
|
|
|
1861
1877
|
governance: buildGovernancePayload(repository, "relation", event.relationId, governanceResult.items[0] ?? repository.getGovernanceStateNullable("relation", event.relationId))
|
|
1862
1878
|
}));
|
|
1863
1879
|
});
|
|
1864
|
-
app.post("/api/v1/search-feedback-events", (request, response) => {
|
|
1880
|
+
app.post("/api/v1/search-feedback-events", handleAsyncRoute(async (request, response) => {
|
|
1865
1881
|
const repository = currentRepository();
|
|
1866
1882
|
const event = repository.appendSearchFeedbackEvent(appendSearchFeedbackSchema.parse(request.body ?? {}));
|
|
1883
|
+
const metadata = event.metadata ?? {};
|
|
1884
|
+
const fallbackMode = readWorkspaceSemanticFallbackMode(metadata.semanticFallbackMode);
|
|
1885
|
+
const lexicalQuality = metadata.lexicalQuality === "strong" || metadata.lexicalQuality === "weak" || metadata.lexicalQuality === "none"
|
|
1886
|
+
? metadata.lexicalQuality
|
|
1887
|
+
: null;
|
|
1888
|
+
const feedbackRank = typeof metadata.rank === "number" && Number.isFinite(metadata.rank)
|
|
1889
|
+
? metadata.rank
|
|
1890
|
+
: typeof metadata.resultRank === "number" && Number.isFinite(metadata.resultRank)
|
|
1891
|
+
? metadata.resultRank
|
|
1892
|
+
: null;
|
|
1893
|
+
const matchStrategy = metadata.matchStrategy === "fts" ||
|
|
1894
|
+
metadata.matchStrategy === "like" ||
|
|
1895
|
+
metadata.matchStrategy === "fallback_token" ||
|
|
1896
|
+
metadata.matchStrategy === "semantic" ||
|
|
1897
|
+
metadata.matchStrategy === "browse"
|
|
1898
|
+
? metadata.matchStrategy
|
|
1899
|
+
: null;
|
|
1900
|
+
await observability.recordEvent({
|
|
1901
|
+
surface: "api",
|
|
1902
|
+
operation: "search.feedback",
|
|
1903
|
+
details: {
|
|
1904
|
+
feedbackVerdict: event.verdict,
|
|
1905
|
+
feedbackResultType: event.resultType,
|
|
1906
|
+
feedbackLexicalQuality: lexicalQuality,
|
|
1907
|
+
feedbackConfidence: event.confidence,
|
|
1908
|
+
feedbackRank,
|
|
1909
|
+
feedbackMatchStrategy: matchStrategy,
|
|
1910
|
+
feedbackSemanticLifted: metadata.semanticLifted === true,
|
|
1911
|
+
feedbackSemanticFallbackMode: fallbackMode ?? undefined,
|
|
1912
|
+
feedbackHasQuery: Boolean(event.query?.trim()),
|
|
1913
|
+
feedbackHasRunId: Boolean(event.runId),
|
|
1914
|
+
feedbackHasSessionId: Boolean(event.sessionId)
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
appendCurrentTelemetryDetails({
|
|
1918
|
+
feedbackVerdict: event.verdict,
|
|
1919
|
+
feedbackResultType: event.resultType,
|
|
1920
|
+
feedbackLexicalQuality: lexicalQuality,
|
|
1921
|
+
feedbackMatchStrategy: matchStrategy,
|
|
1922
|
+
feedbackRank,
|
|
1923
|
+
feedbackSemanticLifted: metadata.semanticLifted === true,
|
|
1924
|
+
feedbackSemanticFallbackMode: fallbackMode ?? undefined,
|
|
1925
|
+
feedbackHasQuery: Boolean(event.query?.trim()),
|
|
1926
|
+
feedbackHasRunId: Boolean(event.runId),
|
|
1927
|
+
feedbackHasSessionId: Boolean(event.sessionId)
|
|
1928
|
+
});
|
|
1867
1929
|
const governanceResult = event.resultType === "node"
|
|
1868
1930
|
? recomputeGovernanceForEntities("node", [event.resultId])
|
|
1869
1931
|
: { items: [] };
|
|
@@ -1878,7 +1940,7 @@ export function createRecallXApp(params) {
|
|
|
1878
1940
|
? buildGovernancePayload(repository, "node", event.resultId, governanceResult.items[0] ?? repository.getGovernanceStateNullable("node", event.resultId))
|
|
1879
1941
|
: null
|
|
1880
1942
|
}));
|
|
1881
|
-
});
|
|
1943
|
+
}));
|
|
1882
1944
|
app.post("/api/v1/inferred-relations/recompute", handleAsyncRoute(async (request, response) => {
|
|
1883
1945
|
const input = recomputeInferredRelationsSchema.parse(request.body ?? {});
|
|
1884
1946
|
const isFullMaintenancePass = !input.generator && !input.relationIds?.length;
|
|
@@ -340,8 +340,44 @@ export class ObservabilityWriter {
|
|
|
340
340
|
const buckets = new Map();
|
|
341
341
|
const mcpFailures = new Map();
|
|
342
342
|
const autoJobs = new Map();
|
|
343
|
+
const searchHitBuckets = new Map();
|
|
344
|
+
const lexicalQualityBuckets = new Map();
|
|
345
|
+
const workspaceFallbackModeBuckets = new Map();
|
|
346
|
+
const feedbackByLexicalQuality = new Map();
|
|
347
|
+
const feedbackByFallbackMode = new Map();
|
|
348
|
+
const semanticFallbackByMode = new Map();
|
|
343
349
|
let ftsFallbackCount = 0;
|
|
344
350
|
let ftsSampleCount = 0;
|
|
351
|
+
let searchHitCount = 0;
|
|
352
|
+
let searchMissCount = 0;
|
|
353
|
+
let searchSampleCount = 0;
|
|
354
|
+
let strongLexicalCount = 0;
|
|
355
|
+
let weakLexicalCount = 0;
|
|
356
|
+
let noLexicalCount = 0;
|
|
357
|
+
let lexicalSampleCount = 0;
|
|
358
|
+
let emptyCompositionCount = 0;
|
|
359
|
+
let nodeOnlyCompositionCount = 0;
|
|
360
|
+
let activityOnlyCompositionCount = 0;
|
|
361
|
+
let mixedCompositionCount = 0;
|
|
362
|
+
let semanticNodeOnlyCompositionCount = 0;
|
|
363
|
+
let semanticMixedCompositionCount = 0;
|
|
364
|
+
let compositionSampleCount = 0;
|
|
365
|
+
let strictZeroFallbackModeCount = 0;
|
|
366
|
+
let noStrongNodeHitFallbackModeCount = 0;
|
|
367
|
+
let workspaceFallbackModeSampleCount = 0;
|
|
368
|
+
let feedbackUsefulCount = 0;
|
|
369
|
+
let feedbackNotUsefulCount = 0;
|
|
370
|
+
let feedbackUncertainCount = 0;
|
|
371
|
+
let feedbackSampleCount = 0;
|
|
372
|
+
let feedbackTop1UsefulCount = 0;
|
|
373
|
+
let feedbackTop1SampleCount = 0;
|
|
374
|
+
let feedbackTop3UsefulCount = 0;
|
|
375
|
+
let feedbackTop3SampleCount = 0;
|
|
376
|
+
let feedbackSemanticUsefulCount = 0;
|
|
377
|
+
let feedbackSemanticNotUsefulCount = 0;
|
|
378
|
+
let feedbackSemanticSampleCount = 0;
|
|
379
|
+
let feedbackSemanticLiftUsefulCount = 0;
|
|
380
|
+
let feedbackSemanticLiftSampleCount = 0;
|
|
345
381
|
let semanticUsedCount = 0;
|
|
346
382
|
let semanticSampleCount = 0;
|
|
347
383
|
let semanticFallbackEligibleCount = 0;
|
|
@@ -354,6 +390,209 @@ export class ObservabilityWriter {
|
|
|
354
390
|
ftsFallbackCount += 1;
|
|
355
391
|
}
|
|
356
392
|
}
|
|
393
|
+
if (typeof event.details.searchHit === "boolean") {
|
|
394
|
+
searchSampleCount += 1;
|
|
395
|
+
if (event.details.searchHit) {
|
|
396
|
+
searchHitCount += 1;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
searchMissCount += 1;
|
|
400
|
+
}
|
|
401
|
+
const bucketKey = `${event.surface}:${event.operation}`;
|
|
402
|
+
const current = searchHitBuckets.get(bucketKey) ?? {
|
|
403
|
+
surface: event.surface,
|
|
404
|
+
operation: event.operation,
|
|
405
|
+
hitCount: 0,
|
|
406
|
+
missCount: 0
|
|
407
|
+
};
|
|
408
|
+
if (event.details.searchHit) {
|
|
409
|
+
current.hitCount += 1;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
current.missCount += 1;
|
|
413
|
+
}
|
|
414
|
+
searchHitBuckets.set(bucketKey, current);
|
|
415
|
+
}
|
|
416
|
+
if (event.details.bestLexicalQuality === "strong" ||
|
|
417
|
+
event.details.bestLexicalQuality === "weak" ||
|
|
418
|
+
event.details.bestLexicalQuality === "none" ||
|
|
419
|
+
event.details.bestNodeLexicalQuality === "strong" ||
|
|
420
|
+
event.details.bestNodeLexicalQuality === "weak" ||
|
|
421
|
+
event.details.bestNodeLexicalQuality === "none") {
|
|
422
|
+
const quality = (event.details.bestNodeLexicalQuality ??
|
|
423
|
+
event.details.bestLexicalQuality);
|
|
424
|
+
lexicalSampleCount += 1;
|
|
425
|
+
if (quality === "strong") {
|
|
426
|
+
strongLexicalCount += 1;
|
|
427
|
+
}
|
|
428
|
+
else if (quality === "weak") {
|
|
429
|
+
weakLexicalCount += 1;
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
noLexicalCount += 1;
|
|
433
|
+
}
|
|
434
|
+
const bucketKey = `${event.surface}:${event.operation}`;
|
|
435
|
+
const current = lexicalQualityBuckets.get(bucketKey) ?? {
|
|
436
|
+
surface: event.surface,
|
|
437
|
+
operation: event.operation,
|
|
438
|
+
strongCount: 0,
|
|
439
|
+
weakCount: 0,
|
|
440
|
+
noneCount: 0
|
|
441
|
+
};
|
|
442
|
+
if (quality === "strong") {
|
|
443
|
+
current.strongCount += 1;
|
|
444
|
+
}
|
|
445
|
+
else if (quality === "weak") {
|
|
446
|
+
current.weakCount += 1;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
current.noneCount += 1;
|
|
450
|
+
}
|
|
451
|
+
lexicalQualityBuckets.set(bucketKey, current);
|
|
452
|
+
}
|
|
453
|
+
if (typeof event.details.resultComposition === "string") {
|
|
454
|
+
compositionSampleCount += 1;
|
|
455
|
+
switch (event.details.resultComposition) {
|
|
456
|
+
case "node_only":
|
|
457
|
+
nodeOnlyCompositionCount += 1;
|
|
458
|
+
break;
|
|
459
|
+
case "activity_only":
|
|
460
|
+
activityOnlyCompositionCount += 1;
|
|
461
|
+
break;
|
|
462
|
+
case "mixed":
|
|
463
|
+
mixedCompositionCount += 1;
|
|
464
|
+
break;
|
|
465
|
+
case "semantic_node_only":
|
|
466
|
+
semanticNodeOnlyCompositionCount += 1;
|
|
467
|
+
break;
|
|
468
|
+
case "semantic_mixed":
|
|
469
|
+
semanticMixedCompositionCount += 1;
|
|
470
|
+
break;
|
|
471
|
+
default:
|
|
472
|
+
emptyCompositionCount += 1;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (event.operation === "workspace.search" &&
|
|
477
|
+
(event.details.semanticFallbackMode === "strict_zero" || event.details.semanticFallbackMode === "no_strong_node_hit")) {
|
|
478
|
+
workspaceFallbackModeSampleCount += 1;
|
|
479
|
+
if (event.details.semanticFallbackMode === "strict_zero") {
|
|
480
|
+
strictZeroFallbackModeCount += 1;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
noStrongNodeHitFallbackModeCount += 1;
|
|
484
|
+
}
|
|
485
|
+
const bucketKey = `${event.surface}:${event.operation}`;
|
|
486
|
+
const current = workspaceFallbackModeBuckets.get(bucketKey) ?? {
|
|
487
|
+
surface: event.surface,
|
|
488
|
+
operation: event.operation,
|
|
489
|
+
strictZeroCount: 0,
|
|
490
|
+
noStrongNodeHitCount: 0
|
|
491
|
+
};
|
|
492
|
+
if (event.details.semanticFallbackMode === "strict_zero") {
|
|
493
|
+
current.strictZeroCount += 1;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
current.noStrongNodeHitCount += 1;
|
|
497
|
+
}
|
|
498
|
+
workspaceFallbackModeBuckets.set(bucketKey, current);
|
|
499
|
+
}
|
|
500
|
+
if (event.details.feedbackVerdict === "useful" ||
|
|
501
|
+
event.details.feedbackVerdict === "not_useful" ||
|
|
502
|
+
event.details.feedbackVerdict === "uncertain") {
|
|
503
|
+
feedbackSampleCount += 1;
|
|
504
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
505
|
+
feedbackUsefulCount += 1;
|
|
506
|
+
}
|
|
507
|
+
else if (event.details.feedbackVerdict === "not_useful") {
|
|
508
|
+
feedbackNotUsefulCount += 1;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
feedbackUncertainCount += 1;
|
|
512
|
+
}
|
|
513
|
+
const feedbackRank = typeof event.details.feedbackRank === "number" && Number.isFinite(event.details.feedbackRank)
|
|
514
|
+
? event.details.feedbackRank
|
|
515
|
+
: null;
|
|
516
|
+
if (feedbackRank != null) {
|
|
517
|
+
if (feedbackRank <= 1) {
|
|
518
|
+
feedbackTop1SampleCount += 1;
|
|
519
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
520
|
+
feedbackTop1UsefulCount += 1;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (feedbackRank <= 3) {
|
|
524
|
+
feedbackTop3SampleCount += 1;
|
|
525
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
526
|
+
feedbackTop3UsefulCount += 1;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const feedbackMatchStrategy = event.details.feedbackMatchStrategy === "fts" ||
|
|
531
|
+
event.details.feedbackMatchStrategy === "like" ||
|
|
532
|
+
event.details.feedbackMatchStrategy === "fallback_token" ||
|
|
533
|
+
event.details.feedbackMatchStrategy === "semantic" ||
|
|
534
|
+
event.details.feedbackMatchStrategy === "browse"
|
|
535
|
+
? event.details.feedbackMatchStrategy
|
|
536
|
+
: null;
|
|
537
|
+
if (feedbackMatchStrategy === "semantic") {
|
|
538
|
+
feedbackSemanticSampleCount += 1;
|
|
539
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
540
|
+
feedbackSemanticUsefulCount += 1;
|
|
541
|
+
}
|
|
542
|
+
else if (event.details.feedbackVerdict === "not_useful") {
|
|
543
|
+
feedbackSemanticNotUsefulCount += 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (event.details.feedbackSemanticLifted === true) {
|
|
547
|
+
feedbackSemanticLiftSampleCount += 1;
|
|
548
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
549
|
+
feedbackSemanticLiftUsefulCount += 1;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const lexicalQuality = event.details.feedbackLexicalQuality === "strong" ||
|
|
553
|
+
event.details.feedbackLexicalQuality === "weak" ||
|
|
554
|
+
event.details.feedbackLexicalQuality === "none"
|
|
555
|
+
? event.details.feedbackLexicalQuality
|
|
556
|
+
: null;
|
|
557
|
+
if (lexicalQuality) {
|
|
558
|
+
const current = feedbackByLexicalQuality.get(lexicalQuality) ?? {
|
|
559
|
+
usefulCount: 0,
|
|
560
|
+
notUsefulCount: 0,
|
|
561
|
+
uncertainCount: 0
|
|
562
|
+
};
|
|
563
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
564
|
+
current.usefulCount += 1;
|
|
565
|
+
}
|
|
566
|
+
else if (event.details.feedbackVerdict === "not_useful") {
|
|
567
|
+
current.notUsefulCount += 1;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
current.uncertainCount += 1;
|
|
571
|
+
}
|
|
572
|
+
feedbackByLexicalQuality.set(lexicalQuality, current);
|
|
573
|
+
}
|
|
574
|
+
const feedbackFallbackMode = event.details.feedbackSemanticFallbackMode === "strict_zero" ||
|
|
575
|
+
event.details.feedbackSemanticFallbackMode === "no_strong_node_hit"
|
|
576
|
+
? event.details.feedbackSemanticFallbackMode
|
|
577
|
+
: null;
|
|
578
|
+
if (feedbackFallbackMode) {
|
|
579
|
+
const current = feedbackByFallbackMode.get(feedbackFallbackMode) ?? {
|
|
580
|
+
usefulCount: 0,
|
|
581
|
+
notUsefulCount: 0,
|
|
582
|
+
uncertainCount: 0
|
|
583
|
+
};
|
|
584
|
+
if (event.details.feedbackVerdict === "useful") {
|
|
585
|
+
current.usefulCount += 1;
|
|
586
|
+
}
|
|
587
|
+
else if (event.details.feedbackVerdict === "not_useful") {
|
|
588
|
+
current.notUsefulCount += 1;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
current.uncertainCount += 1;
|
|
592
|
+
}
|
|
593
|
+
feedbackByFallbackMode.set(feedbackFallbackMode, current);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
357
596
|
if (typeof event.details.semanticUsed === "boolean") {
|
|
358
597
|
semanticSampleCount += 1;
|
|
359
598
|
if (event.details.semanticUsed) {
|
|
@@ -375,6 +614,29 @@ export class ObservabilityWriter {
|
|
|
375
614
|
semanticFallbackHitCount += 1;
|
|
376
615
|
}
|
|
377
616
|
}
|
|
617
|
+
const semanticFallbackMode = event.operation === "workspace.search" &&
|
|
618
|
+
(event.details.semanticFallbackMode === "strict_zero" || event.details.semanticFallbackMode === "no_strong_node_hit")
|
|
619
|
+
? event.details.semanticFallbackMode
|
|
620
|
+
: null;
|
|
621
|
+
if (semanticFallbackMode) {
|
|
622
|
+
const current = semanticFallbackByMode.get(semanticFallbackMode) ?? {
|
|
623
|
+
eligibleCount: 0,
|
|
624
|
+
attemptedCount: 0,
|
|
625
|
+
hitCount: 0,
|
|
626
|
+
sampleCount: 0
|
|
627
|
+
};
|
|
628
|
+
current.sampleCount += 1;
|
|
629
|
+
if (event.details.semanticFallbackEligible === true) {
|
|
630
|
+
current.eligibleCount += 1;
|
|
631
|
+
}
|
|
632
|
+
if (event.details.semanticFallbackAttempted === true) {
|
|
633
|
+
current.attemptedCount += 1;
|
|
634
|
+
}
|
|
635
|
+
if (event.details.semanticFallbackUsed === true) {
|
|
636
|
+
current.hitCount += 1;
|
|
637
|
+
}
|
|
638
|
+
semanticFallbackByMode.set(semanticFallbackMode, current);
|
|
639
|
+
}
|
|
378
640
|
if (event.durationMs != null) {
|
|
379
641
|
const bucketKey = `${event.surface}:${event.operation}`;
|
|
380
642
|
const current = buckets.get(bucketKey) ??
|
|
@@ -440,6 +702,108 @@ export class ObservabilityWriter {
|
|
|
440
702
|
sampleCount: ftsSampleCount,
|
|
441
703
|
ratio: ftsSampleCount > 0 ? Number((ftsFallbackCount / ftsSampleCount).toFixed(4)) : null
|
|
442
704
|
},
|
|
705
|
+
searchHitRate: {
|
|
706
|
+
hitCount: searchHitCount,
|
|
707
|
+
missCount: searchMissCount,
|
|
708
|
+
sampleCount: searchSampleCount,
|
|
709
|
+
ratio: searchSampleCount > 0 ? Number((searchHitCount / searchSampleCount).toFixed(4)) : null,
|
|
710
|
+
operations: [...searchHitBuckets.values()]
|
|
711
|
+
.map((item) => ({
|
|
712
|
+
...item,
|
|
713
|
+
sampleCount: item.hitCount + item.missCount,
|
|
714
|
+
ratio: item.hitCount + item.missCount > 0
|
|
715
|
+
? Number((item.hitCount / (item.hitCount + item.missCount)).toFixed(4))
|
|
716
|
+
: null
|
|
717
|
+
}))
|
|
718
|
+
.sort((left, right) => right.sampleCount - left.sampleCount || left.operation.localeCompare(right.operation))
|
|
719
|
+
},
|
|
720
|
+
searchLexicalQualityRate: {
|
|
721
|
+
strongCount: strongLexicalCount,
|
|
722
|
+
weakCount: weakLexicalCount,
|
|
723
|
+
noneCount: noLexicalCount,
|
|
724
|
+
sampleCount: lexicalSampleCount,
|
|
725
|
+
operations: [...lexicalQualityBuckets.values()]
|
|
726
|
+
.map((item) => ({
|
|
727
|
+
...item,
|
|
728
|
+
sampleCount: item.strongCount + item.weakCount + item.noneCount
|
|
729
|
+
}))
|
|
730
|
+
.sort((left, right) => right.sampleCount - left.sampleCount || left.operation.localeCompare(right.operation))
|
|
731
|
+
},
|
|
732
|
+
workspaceResultCompositionRate: {
|
|
733
|
+
emptyCount: emptyCompositionCount,
|
|
734
|
+
nodeOnlyCount: nodeOnlyCompositionCount,
|
|
735
|
+
activityOnlyCount: activityOnlyCompositionCount,
|
|
736
|
+
mixedCount: mixedCompositionCount,
|
|
737
|
+
semanticNodeOnlyCount: semanticNodeOnlyCompositionCount,
|
|
738
|
+
semanticMixedCount: semanticMixedCompositionCount,
|
|
739
|
+
sampleCount: compositionSampleCount
|
|
740
|
+
},
|
|
741
|
+
workspaceFallbackModeRate: {
|
|
742
|
+
strictZeroCount: strictZeroFallbackModeCount,
|
|
743
|
+
noStrongNodeHitCount: noStrongNodeHitFallbackModeCount,
|
|
744
|
+
sampleCount: workspaceFallbackModeSampleCount,
|
|
745
|
+
operations: [...workspaceFallbackModeBuckets.values()]
|
|
746
|
+
.map((item) => ({
|
|
747
|
+
...item,
|
|
748
|
+
sampleCount: item.strictZeroCount + item.noStrongNodeHitCount
|
|
749
|
+
}))
|
|
750
|
+
.sort((left, right) => right.sampleCount - left.sampleCount || left.operation.localeCompare(right.operation))
|
|
751
|
+
},
|
|
752
|
+
searchFeedbackRate: {
|
|
753
|
+
usefulCount: feedbackUsefulCount,
|
|
754
|
+
notUsefulCount: feedbackNotUsefulCount,
|
|
755
|
+
uncertainCount: feedbackUncertainCount,
|
|
756
|
+
sampleCount: feedbackSampleCount,
|
|
757
|
+
usefulRatio: feedbackSampleCount > 0 ? Number((feedbackUsefulCount / feedbackSampleCount).toFixed(4)) : null,
|
|
758
|
+
top1UsefulCount: feedbackTop1UsefulCount,
|
|
759
|
+
top1SampleCount: feedbackTop1SampleCount,
|
|
760
|
+
top1UsefulRatio: feedbackTop1SampleCount > 0 ? Number((feedbackTop1UsefulCount / feedbackTop1SampleCount).toFixed(4)) : null,
|
|
761
|
+
top3UsefulCount: feedbackTop3UsefulCount,
|
|
762
|
+
top3SampleCount: feedbackTop3SampleCount,
|
|
763
|
+
top3UsefulRatio: feedbackTop3SampleCount > 0 ? Number((feedbackTop3UsefulCount / feedbackTop3SampleCount).toFixed(4)) : null,
|
|
764
|
+
semanticUsefulCount: feedbackSemanticUsefulCount,
|
|
765
|
+
semanticNotUsefulCount: feedbackSemanticNotUsefulCount,
|
|
766
|
+
semanticSampleCount: feedbackSemanticSampleCount,
|
|
767
|
+
semanticUsefulRatio: feedbackSemanticSampleCount > 0 ? Number((feedbackSemanticUsefulCount / feedbackSemanticSampleCount).toFixed(4)) : null,
|
|
768
|
+
semanticFalsePositiveRatio: feedbackSemanticSampleCount > 0 ? Number((feedbackSemanticNotUsefulCount / feedbackSemanticSampleCount).toFixed(4)) : null,
|
|
769
|
+
semanticLiftUsefulCount: feedbackSemanticLiftUsefulCount,
|
|
770
|
+
semanticLiftSampleCount: feedbackSemanticLiftSampleCount,
|
|
771
|
+
semanticLiftUsefulRatio: feedbackSemanticLiftSampleCount > 0
|
|
772
|
+
? Number((feedbackSemanticLiftUsefulCount / feedbackSemanticLiftSampleCount).toFixed(4))
|
|
773
|
+
: null,
|
|
774
|
+
byLexicalQuality: ["strong", "weak", "none"]
|
|
775
|
+
.map((lexicalQuality) => {
|
|
776
|
+
const counts = feedbackByLexicalQuality.get(lexicalQuality) ?? {
|
|
777
|
+
usefulCount: 0,
|
|
778
|
+
notUsefulCount: 0,
|
|
779
|
+
uncertainCount: 0
|
|
780
|
+
};
|
|
781
|
+
const sampleCount = counts.usefulCount + counts.notUsefulCount + counts.uncertainCount;
|
|
782
|
+
return {
|
|
783
|
+
lexicalQuality,
|
|
784
|
+
...counts,
|
|
785
|
+
sampleCount,
|
|
786
|
+
usefulRatio: sampleCount > 0 ? Number((counts.usefulCount / sampleCount).toFixed(4)) : null
|
|
787
|
+
};
|
|
788
|
+
})
|
|
789
|
+
.filter((item) => item.sampleCount > 0),
|
|
790
|
+
byFallbackMode: ["strict_zero", "no_strong_node_hit"]
|
|
791
|
+
.map((fallbackMode) => {
|
|
792
|
+
const counts = feedbackByFallbackMode.get(fallbackMode) ?? {
|
|
793
|
+
usefulCount: 0,
|
|
794
|
+
notUsefulCount: 0,
|
|
795
|
+
uncertainCount: 0
|
|
796
|
+
};
|
|
797
|
+
const sampleCount = counts.usefulCount + counts.notUsefulCount + counts.uncertainCount;
|
|
798
|
+
return {
|
|
799
|
+
fallbackMode,
|
|
800
|
+
...counts,
|
|
801
|
+
sampleCount,
|
|
802
|
+
usefulRatio: sampleCount > 0 ? Number((counts.usefulCount / sampleCount).toFixed(4)) : null
|
|
803
|
+
};
|
|
804
|
+
})
|
|
805
|
+
.filter((item) => item.sampleCount > 0)
|
|
806
|
+
},
|
|
443
807
|
semanticAugmentationRate: {
|
|
444
808
|
usedCount: semanticUsedCount,
|
|
445
809
|
sampleCount: semanticSampleCount,
|
|
@@ -454,7 +818,23 @@ export class ObservabilityWriter {
|
|
|
454
818
|
: null,
|
|
455
819
|
hitRatio: semanticFallbackAttemptedCount > 0
|
|
456
820
|
? Number((semanticFallbackHitCount / semanticFallbackAttemptedCount).toFixed(4))
|
|
457
|
-
: null
|
|
821
|
+
: null,
|
|
822
|
+
modes: ["strict_zero", "no_strong_node_hit"]
|
|
823
|
+
.map((fallbackMode) => {
|
|
824
|
+
const counts = semanticFallbackByMode.get(fallbackMode) ?? {
|
|
825
|
+
eligibleCount: 0,
|
|
826
|
+
attemptedCount: 0,
|
|
827
|
+
hitCount: 0,
|
|
828
|
+
sampleCount: 0
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
fallbackMode,
|
|
832
|
+
...counts,
|
|
833
|
+
attemptRatio: counts.eligibleCount > 0 ? Number((counts.attemptedCount / counts.eligibleCount).toFixed(4)) : null,
|
|
834
|
+
hitRatio: counts.attemptedCount > 0 ? Number((counts.hitCount / counts.attemptedCount).toFixed(4)) : null
|
|
835
|
+
};
|
|
836
|
+
})
|
|
837
|
+
.filter((item) => item.sampleCount > 0)
|
|
458
838
|
},
|
|
459
839
|
autoJobStats: [...autoJobs.entries()].map(([operation, durations]) => ({
|
|
460
840
|
operation,
|
|
@@ -34,6 +34,7 @@ const workspaceInboxSource = {
|
|
|
34
34
|
actorLabel: "RecallX",
|
|
35
35
|
toolName: "recallx-system"
|
|
36
36
|
};
|
|
37
|
+
const DEFAULT_WORKSPACE_SEMANTIC_FALLBACK_MODE = "strict_zero";
|
|
37
38
|
function normalizeSearchText(value) {
|
|
38
39
|
return (value ?? "").normalize("NFKC").toLowerCase();
|
|
39
40
|
}
|
|
@@ -52,32 +53,140 @@ function createSearchFieldMatcher(query) {
|
|
|
52
53
|
matchTerms: tokens.length ? tokens : [trimmedQuery]
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
+
function collectSearchFieldSignals(matcher, candidates) {
|
|
56
57
|
if (!matcher) {
|
|
57
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
matchedFields: [],
|
|
60
|
+
exactFields: [],
|
|
61
|
+
matchedTermCount: 0,
|
|
62
|
+
totalTermCount: 0
|
|
63
|
+
};
|
|
58
64
|
}
|
|
59
|
-
const
|
|
65
|
+
const matchedFields = new Set();
|
|
66
|
+
const exactFields = new Set();
|
|
67
|
+
const matchedTerms = new Set();
|
|
60
68
|
for (const candidate of candidates) {
|
|
61
69
|
const haystack = normalizeSearchText(candidate.value);
|
|
62
70
|
if (!haystack) {
|
|
63
71
|
continue;
|
|
64
72
|
}
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const exactMatch = haystack.includes(matcher.trimmedQuery);
|
|
74
|
+
const termMatches = matcher.matchTerms.filter((term) => haystack.includes(term));
|
|
75
|
+
if (!exactMatch && !termMatches.length) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
matchedFields.add(candidate.field);
|
|
79
|
+
if (exactMatch) {
|
|
80
|
+
exactFields.add(candidate.field);
|
|
81
|
+
}
|
|
82
|
+
for (const term of termMatches) {
|
|
83
|
+
matchedTerms.add(term);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
matchedFields: [...matchedFields],
|
|
88
|
+
exactFields: [...exactFields],
|
|
89
|
+
matchedTermCount: matchedTerms.size,
|
|
90
|
+
totalTermCount: matcher.matchTerms.length
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function classifyNodeLexicalQuality(strategy, signals) {
|
|
94
|
+
if (strategy === "browse" || strategy === "semantic" || !signals.matchedFields.length) {
|
|
95
|
+
return "none";
|
|
96
|
+
}
|
|
97
|
+
if (strategy === "fallback_token") {
|
|
98
|
+
return "weak";
|
|
99
|
+
}
|
|
100
|
+
const strongExactFields = new Set(["title", "summary", "tags"]);
|
|
101
|
+
if (signals.exactFields.some((field) => strongExactFields.has(field))) {
|
|
102
|
+
return "strong";
|
|
103
|
+
}
|
|
104
|
+
const termCoverage = signals.totalTermCount > 0 ? signals.matchedTermCount / signals.totalTermCount : 0;
|
|
105
|
+
if (strategy === "fts" && termCoverage >= 0.6 && signals.matchedFields.some((field) => strongExactFields.has(field))) {
|
|
106
|
+
return "strong";
|
|
107
|
+
}
|
|
108
|
+
return "weak";
|
|
109
|
+
}
|
|
110
|
+
function classifyActivityLexicalQuality(strategy, signals) {
|
|
111
|
+
if (strategy === "browse" || strategy === "semantic" || !signals.matchedFields.length) {
|
|
112
|
+
return "none";
|
|
113
|
+
}
|
|
114
|
+
if (strategy === "fallback_token") {
|
|
115
|
+
return "weak";
|
|
116
|
+
}
|
|
117
|
+
if (signals.exactFields.some((field) => field === "targetNodeTitle" || field === "body" || field === "activityType")) {
|
|
118
|
+
return "strong";
|
|
119
|
+
}
|
|
120
|
+
const termCoverage = signals.totalTermCount > 0 ? signals.matchedTermCount / signals.totalTermCount : 0;
|
|
121
|
+
return strategy === "fts" && termCoverage >= 0.6 ? "strong" : "weak";
|
|
122
|
+
}
|
|
123
|
+
function summarizeLexicalQuality(items) {
|
|
124
|
+
if (items.some((item) => item.lexicalQuality === "strong")) {
|
|
125
|
+
return "strong";
|
|
126
|
+
}
|
|
127
|
+
if (items.some((item) => item.lexicalQuality === "weak")) {
|
|
128
|
+
return "weak";
|
|
129
|
+
}
|
|
130
|
+
return "none";
|
|
131
|
+
}
|
|
132
|
+
function computeWorkspaceResultComposition(input) {
|
|
133
|
+
if (input.nodeCount === 0 && input.activityCount === 0) {
|
|
134
|
+
return "empty";
|
|
135
|
+
}
|
|
136
|
+
if (input.nodeCount > 0 && input.activityCount === 0) {
|
|
137
|
+
return input.semanticUsed ? "semantic_node_only" : "node_only";
|
|
138
|
+
}
|
|
139
|
+
if (input.nodeCount === 0 && input.activityCount > 0) {
|
|
140
|
+
return "activity_only";
|
|
141
|
+
}
|
|
142
|
+
return input.semanticUsed ? "semantic_mixed" : "mixed";
|
|
143
|
+
}
|
|
144
|
+
function mergeLexicalQuality(left, right) {
|
|
145
|
+
if (left === "strong" || right === "strong") {
|
|
146
|
+
return "strong";
|
|
147
|
+
}
|
|
148
|
+
if (left === "weak" || right === "weak") {
|
|
149
|
+
return "weak";
|
|
150
|
+
}
|
|
151
|
+
return "none";
|
|
152
|
+
}
|
|
153
|
+
function mergeNodeSearchItems(primary, secondary) {
|
|
154
|
+
const merged = [...primary];
|
|
155
|
+
const indexById = new Map(primary.map((item, index) => [item.id, index]));
|
|
156
|
+
for (const item of secondary) {
|
|
157
|
+
const existingIndex = indexById.get(item.id);
|
|
158
|
+
if (existingIndex == null) {
|
|
159
|
+
indexById.set(item.id, merged.length);
|
|
160
|
+
merged.push(item);
|
|
161
|
+
continue;
|
|
67
162
|
}
|
|
163
|
+
const existing = merged[existingIndex];
|
|
164
|
+
merged[existingIndex] = {
|
|
165
|
+
...existing,
|
|
166
|
+
lexicalQuality: existing.lexicalQuality === "strong" ? "strong" : item.lexicalQuality ?? existing.lexicalQuality,
|
|
167
|
+
matchReason: existing.matchReason && item.matchReason
|
|
168
|
+
? mergeMatchReasons(existing.matchReason, item.matchReason, existing.matchReason.strategy)
|
|
169
|
+
: existing.matchReason ?? item.matchReason
|
|
170
|
+
};
|
|
68
171
|
}
|
|
69
|
-
return
|
|
172
|
+
return merged;
|
|
70
173
|
}
|
|
71
|
-
function buildSearchMatchReason(strategy, matchedFields) {
|
|
174
|
+
function buildSearchMatchReason(strategy, matchedFields, extras = {}) {
|
|
72
175
|
return {
|
|
73
176
|
strategy,
|
|
74
|
-
matchedFields
|
|
177
|
+
matchedFields,
|
|
178
|
+
...(extras.strength ? { strength: extras.strength } : {}),
|
|
179
|
+
...(extras.termCoverage != null ? { termCoverage: extras.termCoverage } : {})
|
|
75
180
|
};
|
|
76
181
|
}
|
|
77
182
|
function mergeMatchReasons(left, right, strategy) {
|
|
78
183
|
return {
|
|
79
184
|
strategy,
|
|
80
|
-
matchedFields: Array.from(new Set([...(left?.matchedFields ?? []), ...(right?.matchedFields ?? [])]))
|
|
185
|
+
matchedFields: Array.from(new Set([...(left?.matchedFields ?? []), ...(right?.matchedFields ?? [])])),
|
|
186
|
+
strength: left?.strength ?? right?.strength,
|
|
187
|
+
termCoverage: typeof left?.termCoverage === "number" || typeof right?.termCoverage === "number"
|
|
188
|
+
? Math.max(left?.termCoverage ?? 0, right?.termCoverage ?? 0)
|
|
189
|
+
: null
|
|
81
190
|
};
|
|
82
191
|
}
|
|
83
192
|
function computeWorkspaceRankBonus(index, total) {
|
|
@@ -87,8 +196,18 @@ function computeWorkspaceRankBonus(index, total) {
|
|
|
87
196
|
return Math.max(0, Math.round(((total - index) / total) * 24));
|
|
88
197
|
}
|
|
89
198
|
function computeWorkspaceSmartScore(input) {
|
|
199
|
+
const matchBonus = input.matchReason?.strategy === "semantic"
|
|
200
|
+
? 3
|
|
201
|
+
: input.lexicalQuality === "strong"
|
|
202
|
+
? 10
|
|
203
|
+
: input.lexicalQuality === "weak"
|
|
204
|
+
? input.matchReason?.strategy === "fallback_token"
|
|
205
|
+
? 1
|
|
206
|
+
: 4
|
|
207
|
+
: 0;
|
|
90
208
|
return (computeWorkspaceRankBonus(input.index, input.total) +
|
|
91
209
|
computeWorkspaceRecencyBonusFromAge(input.nowMs - new Date(input.timestamp).getTime(), input.resultType) +
|
|
210
|
+
matchBonus +
|
|
92
211
|
(input.resultType === "activity" ? 4 : 0) -
|
|
93
212
|
(input.contested ? 20 : 0));
|
|
94
213
|
}
|
|
@@ -132,6 +251,9 @@ function readNumberSetting(settings, key, fallback) {
|
|
|
132
251
|
const value = settings[key];
|
|
133
252
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
134
253
|
}
|
|
254
|
+
function normalizeWorkspaceSemanticFallbackMode(value) {
|
|
255
|
+
return value === "no_strong_node_hit" ? "no_strong_node_hit" : DEFAULT_WORKSPACE_SEMANTIC_FALLBACK_MODE;
|
|
256
|
+
}
|
|
135
257
|
function normalizeSemanticIndexBackend(value) {
|
|
136
258
|
return value === "sqlite-vec" ? "sqlite-vec" : "sqlite";
|
|
137
259
|
}
|
|
@@ -171,7 +293,9 @@ function readSemanticIndexSettingSnapshot(settings, runtime) {
|
|
|
171
293
|
indexBackend: resolveActiveSemanticIndexBackend(configuredIndexBackend, runtime.sqliteVecLoaded),
|
|
172
294
|
extensionStatus: resolveSemanticExtensionStatus(configuredIndexBackend, runtime.sqliteVecLoaded),
|
|
173
295
|
extensionLoadError: configuredIndexBackend === "sqlite-vec" && !runtime.sqliteVecLoaded ? runtime.sqliteVecLoadError : null,
|
|
174
|
-
chunkEnabled: readBooleanSetting(settings, "search.semantic.chunk.enabled", false)
|
|
296
|
+
chunkEnabled: readBooleanSetting(settings, "search.semantic.chunk.enabled", false),
|
|
297
|
+
workspaceFallbackEnabled: readBooleanSetting(settings, "search.semantic.workspaceFallback.enabled", false),
|
|
298
|
+
workspaceFallbackMode: normalizeWorkspaceSemanticFallbackMode(settings["search.semantic.workspaceFallback.mode"])
|
|
175
299
|
};
|
|
176
300
|
}
|
|
177
301
|
function shouldReindexForSemanticConfigChange(previous, next) {
|
|
@@ -601,7 +725,8 @@ export class RecallXRepository {
|
|
|
601
725
|
"search.semantic.indexBackend",
|
|
602
726
|
"search.semantic.chunk.enabled",
|
|
603
727
|
"search.semantic.chunk.aggregation",
|
|
604
|
-
"search.semantic.workspaceFallback.enabled"
|
|
728
|
+
"search.semantic.workspaceFallback.enabled",
|
|
729
|
+
"search.semantic.workspaceFallback.mode"
|
|
605
730
|
]);
|
|
606
731
|
return {
|
|
607
732
|
...readSemanticIndexSettingSnapshot(settings, {
|
|
@@ -609,7 +734,8 @@ export class RecallXRepository {
|
|
|
609
734
|
sqliteVecLoadError: this.sqliteVecRuntime.loadError
|
|
610
735
|
}),
|
|
611
736
|
chunkAggregation: normalizeSemanticChunkAggregation(settings["search.semantic.chunk.aggregation"]),
|
|
612
|
-
workspaceFallbackEnabled: readBooleanSetting(settings, "search.semantic.workspaceFallback.enabled", false)
|
|
737
|
+
workspaceFallbackEnabled: readBooleanSetting(settings, "search.semantic.workspaceFallback.enabled", false),
|
|
738
|
+
workspaceFallbackMode: normalizeWorkspaceSemanticFallbackMode(settings["search.semantic.workspaceFallback.mode"])
|
|
613
739
|
};
|
|
614
740
|
}
|
|
615
741
|
getSemanticAugmentationSettings() {
|
|
@@ -1010,6 +1136,8 @@ export class RecallXRepository {
|
|
|
1010
1136
|
"search.semantic.model",
|
|
1011
1137
|
"search.semantic.indexBackend",
|
|
1012
1138
|
"search.semantic.chunk.enabled",
|
|
1139
|
+
"search.semantic.workspaceFallback.enabled",
|
|
1140
|
+
"search.semantic.workspaceFallback.mode",
|
|
1013
1141
|
"search.semantic.last_backfill_at"
|
|
1014
1142
|
]);
|
|
1015
1143
|
const semanticSettings = readSemanticIndexSettingSnapshot(settings, {
|
|
@@ -1031,6 +1159,8 @@ export class RecallXRepository {
|
|
|
1031
1159
|
}
|
|
1032
1160
|
return {
|
|
1033
1161
|
...semanticStatusSettings,
|
|
1162
|
+
workspaceFallbackEnabled: readBooleanSetting(settings, "search.semantic.workspaceFallback.enabled", false),
|
|
1163
|
+
workspaceFallbackMode: normalizeWorkspaceSemanticFallbackMode(settings["search.semantic.workspaceFallback.mode"]),
|
|
1034
1164
|
lastBackfillAt: readStringSetting(settings, "search.semantic.last_backfill_at"),
|
|
1035
1165
|
counts
|
|
1036
1166
|
};
|
|
@@ -1303,6 +1433,7 @@ export class RecallXRepository {
|
|
|
1303
1433
|
const result = this.searchNodesWithFts(input);
|
|
1304
1434
|
appendCurrentTelemetryDetails({
|
|
1305
1435
|
ftsFallback: false,
|
|
1436
|
+
lexicalQuality: summarizeLexicalQuality(result.items),
|
|
1306
1437
|
resultCount: result.items.length,
|
|
1307
1438
|
totalCount: result.total
|
|
1308
1439
|
});
|
|
@@ -1312,6 +1443,7 @@ export class RecallXRepository {
|
|
|
1312
1443
|
const fallbackResult = this.searchNodesWithLike(input);
|
|
1313
1444
|
appendCurrentTelemetryDetails({
|
|
1314
1445
|
ftsFallback: true,
|
|
1446
|
+
lexicalQuality: summarizeLexicalQuality(fallbackResult.items),
|
|
1315
1447
|
resultCount: fallbackResult.items.length,
|
|
1316
1448
|
totalCount: fallbackResult.total
|
|
1317
1449
|
});
|
|
@@ -1321,6 +1453,7 @@ export class RecallXRepository {
|
|
|
1321
1453
|
const result = this.searchNodesWithLike(input);
|
|
1322
1454
|
appendCurrentTelemetryDetails({
|
|
1323
1455
|
ftsFallback: false,
|
|
1456
|
+
lexicalQuality: summarizeLexicalQuality(result.items),
|
|
1324
1457
|
resultCount: result.items.length,
|
|
1325
1458
|
totalCount: result.total
|
|
1326
1459
|
});
|
|
@@ -1332,6 +1465,7 @@ export class RecallXRepository {
|
|
|
1332
1465
|
const result = this.searchActivitiesWithFts(input);
|
|
1333
1466
|
appendCurrentTelemetryDetails({
|
|
1334
1467
|
ftsFallback: false,
|
|
1468
|
+
lexicalQuality: summarizeLexicalQuality(result.items),
|
|
1335
1469
|
resultCount: result.items.length,
|
|
1336
1470
|
totalCount: result.total
|
|
1337
1471
|
});
|
|
@@ -1341,6 +1475,7 @@ export class RecallXRepository {
|
|
|
1341
1475
|
const fallbackResult = this.searchActivitiesWithLike(input);
|
|
1342
1476
|
appendCurrentTelemetryDetails({
|
|
1343
1477
|
ftsFallback: true,
|
|
1478
|
+
lexicalQuality: summarizeLexicalQuality(fallbackResult.items),
|
|
1344
1479
|
resultCount: fallbackResult.items.length,
|
|
1345
1480
|
totalCount: fallbackResult.total
|
|
1346
1481
|
});
|
|
@@ -1350,6 +1485,7 @@ export class RecallXRepository {
|
|
|
1350
1485
|
const result = this.searchActivitiesWithLike(input);
|
|
1351
1486
|
appendCurrentTelemetryDetails({
|
|
1352
1487
|
ftsFallback: false,
|
|
1488
|
+
lexicalQuality: summarizeLexicalQuality(result.items),
|
|
1353
1489
|
resultCount: result.items.length,
|
|
1354
1490
|
totalCount: result.total
|
|
1355
1491
|
});
|
|
@@ -1465,6 +1601,8 @@ export class RecallXRepository {
|
|
|
1465
1601
|
const resolvedActivityResults = fallbackTokens.length >= 2 && includeActivities
|
|
1466
1602
|
? this.searchWorkspaceActivityFallback(fallbackTokens, input.activityFilters ?? {}, requestedWindow)
|
|
1467
1603
|
: activityResults;
|
|
1604
|
+
const bestNodeLexicalQuality = summarizeLexicalQuality(resolvedNodeResults.items);
|
|
1605
|
+
const bestActivityLexicalQuality = summarizeLexicalQuality(resolvedActivityResults.items);
|
|
1468
1606
|
const merged = this.mergeWorkspaceSearchResults(resolvedNodeResults.items, resolvedActivityResults.items, input.sort);
|
|
1469
1607
|
const deterministicResult = {
|
|
1470
1608
|
total: fallbackTokens.length >= 2
|
|
@@ -1477,6 +1615,7 @@ export class RecallXRepository {
|
|
|
1477
1615
|
semanticFallbackEligible: false,
|
|
1478
1616
|
semanticFallbackAttempted: false,
|
|
1479
1617
|
semanticFallbackUsed: false,
|
|
1618
|
+
semanticFallbackMode: includeNodes && semanticSettings.workspaceFallbackEnabled ? semanticSettings.workspaceFallbackMode : null,
|
|
1480
1619
|
semanticFallbackCandidateCount: 0,
|
|
1481
1620
|
semanticFallbackResultCount: 0,
|
|
1482
1621
|
semanticFallbackBackend: null,
|
|
@@ -1485,16 +1624,31 @@ export class RecallXRepository {
|
|
|
1485
1624
|
semanticFallbackQueryLengthBucket: queryPresent ? bucketSemanticQueryLength(normalizedQuery.length) : null
|
|
1486
1625
|
};
|
|
1487
1626
|
const appendWorkspaceSearchTelemetry = (result) => {
|
|
1627
|
+
const nodeItems = result.items.flatMap((item) => item.resultType === "node" && item.node ? [item.node] : []);
|
|
1628
|
+
const activityItems = result.items.flatMap((item) => item.resultType === "activity" && item.activity ? [item.activity] : []);
|
|
1488
1629
|
appendCurrentTelemetryDetails({
|
|
1630
|
+
searchHit: result.items.length > 0,
|
|
1489
1631
|
candidateCount: requestedWindow,
|
|
1490
1632
|
nodeCandidateCount: resolvedNodeResults.items.length,
|
|
1491
1633
|
activityCandidateCount: resolvedActivityResults.items.length,
|
|
1634
|
+
nodeResultCount: nodeItems.length,
|
|
1635
|
+
activityResultCount: activityItems.length,
|
|
1636
|
+
bestNodeLexicalQuality,
|
|
1637
|
+
bestActivityLexicalQuality,
|
|
1638
|
+
lexicalNodeHit: bestNodeLexicalQuality !== "none",
|
|
1639
|
+
strongNodeLexicalHit: bestNodeLexicalQuality === "strong",
|
|
1640
|
+
resultComposition: computeWorkspaceResultComposition({
|
|
1641
|
+
nodeCount: nodeItems.length,
|
|
1642
|
+
activityCount: activityItems.length,
|
|
1643
|
+
semanticUsed: telemetry.semanticFallbackUsed
|
|
1644
|
+
}),
|
|
1492
1645
|
resultCount: result.items.length,
|
|
1493
1646
|
totalCount: result.total,
|
|
1494
1647
|
fallbackTokenCount: fallbackTokens.length,
|
|
1495
1648
|
semanticFallbackEligible: telemetry.semanticFallbackEligible,
|
|
1496
1649
|
semanticFallbackAttempted: telemetry.semanticFallbackAttempted,
|
|
1497
1650
|
semanticFallbackUsed: telemetry.semanticFallbackUsed,
|
|
1651
|
+
semanticFallbackMode: telemetry.semanticFallbackMode ?? undefined,
|
|
1498
1652
|
semanticFallbackCandidateCount: telemetry.semanticFallbackCandidateCount,
|
|
1499
1653
|
semanticFallbackResultCount: telemetry.semanticFallbackResultCount,
|
|
1500
1654
|
semanticFallbackBackend: telemetry.semanticFallbackBackend,
|
|
@@ -1502,13 +1656,18 @@ export class RecallXRepository {
|
|
|
1502
1656
|
semanticFallbackSkippedReason: telemetry.semanticFallbackSkippedReason
|
|
1503
1657
|
});
|
|
1504
1658
|
};
|
|
1659
|
+
const strictZeroFallbackBlocked = resolvedNodeResults.total + resolvedActivityResults.total > 0;
|
|
1660
|
+
const noStrongNodeFallbackBlocked = bestNodeLexicalQuality === "strong";
|
|
1661
|
+
const semanticFallbackBlockedByMode = semanticSettings.workspaceFallbackMode === "strict_zero"
|
|
1662
|
+
? strictZeroFallbackBlocked
|
|
1663
|
+
: noStrongNodeFallbackBlocked;
|
|
1505
1664
|
const shouldAttemptSemanticFallback = includeNodes &&
|
|
1506
1665
|
semanticSettings.workspaceFallbackEnabled &&
|
|
1507
1666
|
queryPresent &&
|
|
1508
1667
|
normalizedQuery.length >= 6 &&
|
|
1509
|
-
deterministicResult.total === 0 &&
|
|
1510
1668
|
semanticSettings.enabled &&
|
|
1511
|
-
Boolean(semanticSettings.provider && semanticSettings.model)
|
|
1669
|
+
Boolean(semanticSettings.provider && semanticSettings.model) &&
|
|
1670
|
+
!semanticFallbackBlockedByMode;
|
|
1512
1671
|
if (!includeNodes) {
|
|
1513
1672
|
telemetry.semanticFallbackSkippedReason = "nodes_scope_disabled";
|
|
1514
1673
|
}
|
|
@@ -1521,15 +1680,18 @@ export class RecallXRepository {
|
|
|
1521
1680
|
else if (!semanticSettings.workspaceFallbackEnabled) {
|
|
1522
1681
|
telemetry.semanticFallbackSkippedReason = "workspace_fallback_disabled";
|
|
1523
1682
|
}
|
|
1524
|
-
else if (deterministicResult.total > 0) {
|
|
1525
|
-
telemetry.semanticFallbackSkippedReason = "deterministic_results_present";
|
|
1526
|
-
}
|
|
1527
1683
|
else if (!semanticSettings.enabled) {
|
|
1528
1684
|
telemetry.semanticFallbackSkippedReason = "semantic_disabled";
|
|
1529
1685
|
}
|
|
1530
1686
|
else if (!semanticSettings.provider || !semanticSettings.model) {
|
|
1531
1687
|
telemetry.semanticFallbackSkippedReason = "semantic_provider_unconfigured";
|
|
1532
1688
|
}
|
|
1689
|
+
else if (semanticSettings.workspaceFallbackMode === "strict_zero" && strictZeroFallbackBlocked) {
|
|
1690
|
+
telemetry.semanticFallbackSkippedReason = "strict_zero_results_present";
|
|
1691
|
+
}
|
|
1692
|
+
else if (semanticSettings.workspaceFallbackMode === "no_strong_node_hit" && noStrongNodeFallbackBlocked) {
|
|
1693
|
+
telemetry.semanticFallbackSkippedReason = "strong_node_lexical_present";
|
|
1694
|
+
}
|
|
1533
1695
|
if (shouldAttemptSemanticFallback) {
|
|
1534
1696
|
const candidateNodeIds = this.listWorkspaceSemanticFallbackCandidateNodeIds(input.nodeFilters ?? {}, semanticSettings, 200);
|
|
1535
1697
|
telemetry.semanticFallbackEligible = true;
|
|
@@ -1541,8 +1703,7 @@ export class RecallXRepository {
|
|
|
1541
1703
|
else {
|
|
1542
1704
|
telemetry.semanticFallbackAttempted = true;
|
|
1543
1705
|
const runSemanticFallback = async () => {
|
|
1544
|
-
const
|
|
1545
|
-
const items = this.buildWorkspaceSemanticFallbackNodeItems(candidateNodeIds, semanticMatches, this.getSemanticAugmentationSettings()).map((node) => ({ resultType: "node", node }));
|
|
1706
|
+
const items = this.buildWorkspaceSemanticFallbackNodeItems(candidateNodeIds, await this.rankSemanticCandidates(normalizedQuery, candidateNodeIds), this.getSemanticAugmentationSettings());
|
|
1546
1707
|
return {
|
|
1547
1708
|
items,
|
|
1548
1709
|
resultCount: items.length
|
|
@@ -1554,15 +1715,18 @@ export class RecallXRepository {
|
|
|
1554
1715
|
semanticFallbackCandidateCount: candidateNodeIds.length,
|
|
1555
1716
|
semanticFallbackBackend: semanticSettings.indexBackend,
|
|
1556
1717
|
semanticFallbackConfiguredBackend: semanticSettings.configuredIndexBackend,
|
|
1718
|
+
semanticFallbackMode: telemetry.semanticFallbackMode ?? undefined,
|
|
1557
1719
|
semanticFallbackQueryLengthBucket: telemetry.semanticFallbackQueryLengthBucket
|
|
1558
1720
|
}, runSemanticFallback)
|
|
1559
1721
|
: await runSemanticFallback();
|
|
1560
1722
|
telemetry.semanticFallbackResultCount = semanticResult.resultCount;
|
|
1561
1723
|
if (semanticResult.resultCount > 0) {
|
|
1562
1724
|
telemetry.semanticFallbackUsed = true;
|
|
1725
|
+
const mergedNodeItems = mergeNodeSearchItems(semanticResult.items, resolvedNodeResults.items);
|
|
1726
|
+
const mergedSemanticItems = this.mergeWorkspaceSearchResults(mergedNodeItems, resolvedActivityResults.items, input.sort);
|
|
1563
1727
|
const semanticWorkspaceResult = {
|
|
1564
|
-
total:
|
|
1565
|
-
items:
|
|
1728
|
+
total: mergedNodeItems.length + (includeActivities ? resolvedActivityResults.total : 0),
|
|
1729
|
+
items: mergedSemanticItems.slice(input.offset, input.offset + input.limit)
|
|
1566
1730
|
};
|
|
1567
1731
|
appendWorkspaceSearchTelemetry(semanticWorkspaceResult);
|
|
1568
1732
|
return {
|
|
@@ -1638,6 +1802,8 @@ export class RecallXRepository {
|
|
|
1638
1802
|
timestamp: node.updatedAt,
|
|
1639
1803
|
resultType: "node",
|
|
1640
1804
|
contested: node.status === "contested",
|
|
1805
|
+
matchReason: node.matchReason,
|
|
1806
|
+
lexicalQuality: node.lexicalQuality,
|
|
1641
1807
|
nowMs
|
|
1642
1808
|
})
|
|
1643
1809
|
: 0
|
|
@@ -1656,6 +1822,8 @@ export class RecallXRepository {
|
|
|
1656
1822
|
timestamp: activity.createdAt,
|
|
1657
1823
|
resultType: "activity",
|
|
1658
1824
|
contested: activity.targetNodeStatus === "contested",
|
|
1825
|
+
matchReason: activity.matchReason,
|
|
1826
|
+
lexicalQuality: activity.lexicalQuality,
|
|
1659
1827
|
nowMs
|
|
1660
1828
|
})
|
|
1661
1829
|
: 0
|
|
@@ -1673,6 +1841,24 @@ export class RecallXRepository {
|
|
|
1673
1841
|
}
|
|
1674
1842
|
return merged.map(({ index: _index, total: _total, timestamp: _timestamp, contested: _contested, smartScore: _smartScore, ...item }) => item);
|
|
1675
1843
|
}
|
|
1844
|
+
mergeNodeSearchResults(primary, secondary) {
|
|
1845
|
+
const merged = new Map();
|
|
1846
|
+
for (const item of [...primary, ...secondary]) {
|
|
1847
|
+
const current = merged.get(item.id);
|
|
1848
|
+
if (!current) {
|
|
1849
|
+
merged.set(item.id, item);
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
merged.set(item.id, {
|
|
1853
|
+
...current,
|
|
1854
|
+
matchReason: current.matchReason?.strategy === "semantic" && item.matchReason
|
|
1855
|
+
? item.matchReason
|
|
1856
|
+
: current.matchReason ?? item.matchReason,
|
|
1857
|
+
lexicalQuality: mergeLexicalQuality(current.lexicalQuality, item.lexicalQuality)
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
return Array.from(merged.values());
|
|
1861
|
+
}
|
|
1676
1862
|
searchNodesWithFts(input) {
|
|
1677
1863
|
const where = [];
|
|
1678
1864
|
const values = [];
|
|
@@ -1851,23 +2037,31 @@ export class RecallXRepository {
|
|
|
1851
2037
|
LIMIT ? OFFSET ?`)
|
|
1852
2038
|
.all(...whereValues, ...params.orderValues, effectiveLimit, effectiveOffset);
|
|
1853
2039
|
const matcher = params.strategy === "browse" ? null : createSearchFieldMatcher(params.input.query);
|
|
1854
|
-
const items = rows.map((row) =>
|
|
1855
|
-
|
|
1856
|
-
targetNodeId: String(row.target_node_id),
|
|
1857
|
-
targetNodeTitle: row.target_title ? String(row.target_title) : null,
|
|
1858
|
-
targetNodeType: row.target_type ? row.target_type : null,
|
|
1859
|
-
targetNodeStatus: row.target_status ? row.target_status : null,
|
|
1860
|
-
activityType: row.activity_type,
|
|
1861
|
-
body: row.body ? String(row.body) : null,
|
|
1862
|
-
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
1863
|
-
createdAt: String(row.created_at),
|
|
1864
|
-
matchReason: buildSearchMatchReason(params.strategy, collectMatchedFields(matcher, [
|
|
2040
|
+
const items = rows.map((row) => {
|
|
2041
|
+
const signals = collectSearchFieldSignals(matcher, [
|
|
1865
2042
|
{ field: "body", value: row.body ? String(row.body) : null },
|
|
1866
2043
|
{ field: "targetNodeTitle", value: row.target_title ? String(row.target_title) : null },
|
|
1867
2044
|
{ field: "activityType", value: row.activity_type ? String(row.activity_type) : null },
|
|
1868
2045
|
{ field: "sourceLabel", value: row.source_label ? String(row.source_label) : null }
|
|
1869
|
-
])
|
|
1870
|
-
|
|
2046
|
+
]);
|
|
2047
|
+
const lexicalQuality = classifyActivityLexicalQuality(params.strategy, signals);
|
|
2048
|
+
return {
|
|
2049
|
+
id: String(row.id),
|
|
2050
|
+
targetNodeId: String(row.target_node_id),
|
|
2051
|
+
targetNodeTitle: row.target_title ? String(row.target_title) : null,
|
|
2052
|
+
targetNodeType: row.target_type ? row.target_type : null,
|
|
2053
|
+
targetNodeStatus: row.target_status ? row.target_status : null,
|
|
2054
|
+
activityType: row.activity_type,
|
|
2055
|
+
body: row.body ? String(row.body) : null,
|
|
2056
|
+
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
2057
|
+
createdAt: String(row.created_at),
|
|
2058
|
+
lexicalQuality,
|
|
2059
|
+
matchReason: buildSearchMatchReason(params.strategy, signals.matchedFields, {
|
|
2060
|
+
strength: lexicalQuality === "none" ? undefined : lexicalQuality,
|
|
2061
|
+
termCoverage: signals.totalTermCount > 0 ? Number((signals.matchedTermCount / signals.totalTermCount).toFixed(4)) : null
|
|
2062
|
+
})
|
|
2063
|
+
};
|
|
2064
|
+
});
|
|
1871
2065
|
const rankedItems = useSearchFeedbackBoost ? this.applyActivitySearchFeedbackBoost(items) : items;
|
|
1872
2066
|
const cappedItems = this.capActivityResultsPerTarget(rankedItems);
|
|
1873
2067
|
return {
|
|
@@ -1914,6 +2108,14 @@ export class RecallXRepository {
|
|
|
1914
2108
|
const matcher = strategy === "browse" ? null : createSearchFieldMatcher(query);
|
|
1915
2109
|
const items = rows.map((row) => {
|
|
1916
2110
|
const tags = parseJson(row.tags_json, []);
|
|
2111
|
+
const signals = collectSearchFieldSignals(matcher, [
|
|
2112
|
+
{ field: "title", value: row.title ? String(row.title) : null },
|
|
2113
|
+
{ field: "summary", value: row.summary ? String(row.summary) : null },
|
|
2114
|
+
{ field: "body", value: row.body ? String(row.body) : null },
|
|
2115
|
+
{ field: "tags", value: tags.join(" ") },
|
|
2116
|
+
{ field: "sourceLabel", value: row.source_label ? String(row.source_label) : null }
|
|
2117
|
+
]);
|
|
2118
|
+
const lexicalQuality = classifyNodeLexicalQuality(strategy, signals);
|
|
1917
2119
|
return {
|
|
1918
2120
|
id: String(row.id),
|
|
1919
2121
|
type: row.type,
|
|
@@ -1924,13 +2126,11 @@ export class RecallXRepository {
|
|
|
1924
2126
|
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
1925
2127
|
updatedAt: String(row.updated_at),
|
|
1926
2128
|
tags,
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
{ field: "sourceLabel", value: row.source_label ? String(row.source_label) : null }
|
|
1933
|
-
]))
|
|
2129
|
+
lexicalQuality,
|
|
2130
|
+
matchReason: buildSearchMatchReason(strategy, signals.matchedFields, {
|
|
2131
|
+
strength: lexicalQuality === "none" ? undefined : lexicalQuality,
|
|
2132
|
+
termCoverage: signals.totalTermCount > 0 ? Number((signals.matchedTermCount / signals.totalTermCount).toFixed(4)) : null
|
|
2133
|
+
})
|
|
1934
2134
|
};
|
|
1935
2135
|
});
|
|
1936
2136
|
const rankedItems = useSearchFeedbackBoost ? this.applySearchFeedbackBoost(items) : items;
|
|
@@ -76,6 +76,7 @@ export class WorkspaceSessionManager {
|
|
|
76
76
|
"search.semantic.chunk.enabled": false,
|
|
77
77
|
"search.semantic.chunk.aggregation": "max",
|
|
78
78
|
"search.semantic.workspaceFallback.enabled": false,
|
|
79
|
+
"search.semantic.workspaceFallback.mode": "strict_zero",
|
|
79
80
|
"search.semantic.augmentation.minSimilarity": 0.2,
|
|
80
81
|
"search.semantic.augmentation.maxBonus": 18,
|
|
81
82
|
"search.semantic.last_backfill_at": null,
|
package/app/shared/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RECALLX_VERSION = "1.0.
|
|
1
|
+
export const RECALLX_VERSION = "1.0.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recallx-headless",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Headless RecallX runtime with API, CLI, and MCP entrypoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"zod": "^3.25.76"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=22.13.0"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
|
30
30
|
"recallx",
|