recallx 1.0.5 → 1.0.8
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 +4 -0
- package/app/cli/src/format.js +1 -1
- package/app/mcp/api-client.js +5 -1
- package/app/mcp/index.js +1 -1
- package/app/mcp/server.js +24 -6
- package/app/server/app.js +47 -17
- package/app/server/governance.js +1 -1
- package/app/server/inferred-relations.js +23 -10
- package/app/server/observability.js +82 -21
- package/app/server/repositories.js +11 -0
- package/app/server/retrieval.js +84 -4
- package/app/shared/contracts.js +50 -2
- package/app/shared/version.js +1 -1
- package/dist/renderer/assets/{ProjectGraphCanvas-Crp1MklO.js → ProjectGraphCanvas-BLmjIT0R.js} +1 -1
- package/dist/renderer/assets/{index-CSO0evPr.js → index-CIY8bKYQ.js} +16 -16
- package/dist/renderer/index.html +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
|
@@ -225,3 +225,7 @@ For launcher paths, environment variables, and editor-specific setup, see `docs/
|
|
|
225
225
|
- `docs/mcp.md` for MCP bridge setup
|
|
226
226
|
- `docs/workflows.md` for common usage flows
|
|
227
227
|
- `docs/schema.md` for storage and data model details
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT. See `LICENSE`.
|
package/app/cli/src/format.js
CHANGED
|
@@ -304,7 +304,7 @@ export function renderTelemetryErrors(data) {
|
|
|
304
304
|
return `${items
|
|
305
305
|
.map(
|
|
306
306
|
(item, index) =>
|
|
307
|
-
`${index + 1}. [${item.surface}] ${item.operation}\n ts: ${item.ts}\n trace: ${item.traceId}\n error: ${item.errorKind || ""}/${item.errorCode || ""}\n status: ${item.statusCode ?? ""}\n durationMs: ${item.durationMs ?? ""}`
|
|
307
|
+
`${index + 1}. [${item.surface}] ${item.operation}\n ts: ${item.ts}\n trace: ${item.traceId}\n span: ${item.spanId ?? ""}\n parent: ${item.parentSpanId ?? ""}\n error: ${item.errorKind || ""}/${item.errorCode || ""}\n status: ${item.statusCode ?? ""}\n durationMs: ${item.durationMs ?? ""}`
|
|
308
308
|
)
|
|
309
309
|
.join("\n\n")}\n`;
|
|
310
310
|
}
|
package/app/mcp/api-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildApiRequestInit, buildApiUrl, parseApiJsonBody } from "../shared/request-runtime.js";
|
|
2
|
-
import { currentTelemetryContext } from "../server/observability.js";
|
|
2
|
+
import { currentTelemetryContext, currentTelemetrySpanId } from "../server/observability.js";
|
|
3
3
|
export class RecallXApiError extends Error {
|
|
4
4
|
status;
|
|
5
5
|
code;
|
|
@@ -43,6 +43,10 @@ export class RecallXApiClient {
|
|
|
43
43
|
if (telemetryContext?.traceId) {
|
|
44
44
|
headers.set("x-recallx-trace-id", telemetryContext.traceId);
|
|
45
45
|
}
|
|
46
|
+
const telemetrySpanId = currentTelemetrySpanId();
|
|
47
|
+
if (telemetrySpanId) {
|
|
48
|
+
headers.set("x-recallx-parent-span-id", telemetrySpanId);
|
|
49
|
+
}
|
|
46
50
|
if (telemetryContext?.toolName) {
|
|
47
51
|
headers.set("x-recallx-mcp-tool", telemetryContext.toolName);
|
|
48
52
|
}
|
package/app/mcp/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function createObservabilityStateReader() {
|
|
|
10
10
|
let cachedState = null;
|
|
11
11
|
let cachedAt = 0;
|
|
12
12
|
let inFlight = null;
|
|
13
|
-
const cacheTtlMs =
|
|
13
|
+
const cacheTtlMs = 60_000;
|
|
14
14
|
return async function readObservabilityState() {
|
|
15
15
|
const now = Date.now();
|
|
16
16
|
if (cachedState && now - cachedAt < cacheTtlMs) {
|
package/app/mcp/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
|
-
import { activityTypes, bundleModes, bundlePresets, canonicalities, captureModes, governanceStates, inferredRelationStatuses, nodeStatuses, nodeTypes, relationSources, relationStatuses, relationTypes, relationUsageEventTypes, searchFeedbackResultTypes, searchFeedbackVerdicts, sourceTypes } from "../shared/contracts.js";
|
|
3
|
+
import { activityTypes, bundleModes, bundlePresets, canonicalities, captureModes, governanceStates, inferredRelationStatuses, normalizeBundleMode, normalizeBundlePreset, nodeStatuses, nodeTypes, relationSources, relationStatuses, relationTypes, relationUsageEventTypes, searchFeedbackResultTypes, searchFeedbackVerdicts, sourceTypes } from "../shared/contracts.js";
|
|
4
4
|
import { RECALLX_VERSION } from "../shared/version.js";
|
|
5
5
|
import { createObservabilityWriter, summarizePayloadShape } from "../server/observability.js";
|
|
6
6
|
import { RecallXApiClient, RecallXApiError } from "./api-client.js";
|
|
@@ -67,6 +67,24 @@ function coerceBooleanSchema(defaultValue) {
|
|
|
67
67
|
function formatStructuredContent(content) {
|
|
68
68
|
return JSON.stringify(content, null, 2);
|
|
69
69
|
}
|
|
70
|
+
function formatInvalidBundleModeMessage(input) {
|
|
71
|
+
const quotedInput = typeof input === "string" && input.trim() ? `'${input}'` : "that value";
|
|
72
|
+
return `Unsupported mode ${quotedInput}. Use one of ${bundleModes.join(", ")}. Common aliases also work: small -> micro, concise -> compact, normal -> standard, full -> deep.`;
|
|
73
|
+
}
|
|
74
|
+
function formatInvalidBundlePresetMessage(input) {
|
|
75
|
+
const quotedInput = typeof input === "string" && input.trim() ? `'${input}'` : "that value";
|
|
76
|
+
return `Unsupported preset ${quotedInput}. Use one of ${bundlePresets.join(", ")}. Common aliases also work: coding -> for-coding, assistant/default -> for-assistant.`;
|
|
77
|
+
}
|
|
78
|
+
function bundleModeSchema(defaultValue) {
|
|
79
|
+
return z.preprocess(normalizeBundleMode, z.enum(bundleModes, {
|
|
80
|
+
error: (issue) => formatInvalidBundleModeMessage(issue.input)
|
|
81
|
+
})).default(defaultValue);
|
|
82
|
+
}
|
|
83
|
+
function bundlePresetSchema(defaultValue) {
|
|
84
|
+
return z.preprocess(normalizeBundlePreset, z.enum(bundlePresets, {
|
|
85
|
+
error: (issue) => formatInvalidBundlePresetMessage(issue.input)
|
|
86
|
+
})).default(defaultValue);
|
|
87
|
+
}
|
|
70
88
|
function toolResult(structuredContent) {
|
|
71
89
|
return {
|
|
72
90
|
content: [
|
|
@@ -317,7 +335,7 @@ export function createRecallXMcpServer(params) {
|
|
|
317
335
|
name: "recallx-mcp",
|
|
318
336
|
version: params?.serverVersion ?? RECALLX_VERSION
|
|
319
337
|
}, {
|
|
320
|
-
instructions: "Use RecallX as a local knowledge backend. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. When the work is clearly project-shaped, search for an existing project inside the current workspace first: prefer recallx_search_nodes with type=project, broaden with recallx_search_workspace when needed, create a project node only if no suitable one exists, and then anchor follow-up context with recallx_context_bundle targetId. If the conversation is not project-specific, keep memory at workspace scope. Prefer read tools first, and include source details on durable writes when you want caller-specific provenance.",
|
|
338
|
+
instructions: "Use RecallX as a local knowledge backend. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. When the work is clearly project-shaped, search for an existing project inside the current workspace first: prefer recallx_search_nodes with type=project, broaden with recallx_search_workspace when needed, create a project node only if no suitable one exists, and then anchor follow-up context with recallx_context_bundle targetId. 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. If the conversation is not project-specific, keep memory at workspace scope. Prefer read tools first, and include source details on durable writes when you want caller-specific provenance.",
|
|
321
339
|
capabilities: {
|
|
322
340
|
logging: {}
|
|
323
341
|
}
|
|
@@ -617,7 +635,7 @@ export function createRecallXMcpServer(params) {
|
|
|
617
635
|
}, createPostToolHandler(apiClient, "/activities"));
|
|
618
636
|
registerTool(server, "recallx_capture_memory", {
|
|
619
637
|
title: "Capture Memory",
|
|
620
|
-
description: "Safely capture a memory item without choosing low-level storage first. Prefer this as the default write when the conversation is not yet tied to a specific project or node. General short logs can stay at workspace scope and be auto-routed into activities, while reusable content can still land as durable memory.",
|
|
638
|
+
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.",
|
|
621
639
|
inputSchema: {
|
|
622
640
|
mode: z.enum(captureModes).default("auto"),
|
|
623
641
|
body: z.string().min(1),
|
|
@@ -726,8 +744,8 @@ export function createRecallXMcpServer(params) {
|
|
|
726
744
|
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.",
|
|
727
745
|
inputSchema: {
|
|
728
746
|
targetId: z.string().min(1).optional(),
|
|
729
|
-
mode:
|
|
730
|
-
preset:
|
|
747
|
+
mode: bundleModeSchema("compact"),
|
|
748
|
+
preset: bundlePresetSchema("for-assistant"),
|
|
731
749
|
options: z
|
|
732
750
|
.object({
|
|
733
751
|
includeRelated: coerceBooleanSchema(true),
|
|
@@ -778,7 +796,7 @@ export function createRecallXMcpServer(params) {
|
|
|
778
796
|
inputSchema: {
|
|
779
797
|
query: z.string().default(""),
|
|
780
798
|
candidateNodeIds: z.array(z.string().min(1)).min(1).max(100),
|
|
781
|
-
preset:
|
|
799
|
+
preset: bundlePresetSchema("for-assistant"),
|
|
782
800
|
targetNodeId: z.string().optional()
|
|
783
801
|
}
|
|
784
802
|
}, createPostToolHandler(apiClient, "/retrieval/rank-candidates"));
|
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);
|
|
@@ -1160,6 +1160,25 @@ export function createRecallXApp(params) {
|
|
|
1160
1160
|
reason: input.reason
|
|
1161
1161
|
};
|
|
1162
1162
|
}
|
|
1163
|
+
function resolveCaptureActivityTarget(repository, input) {
|
|
1164
|
+
if (input.targetNodeId) {
|
|
1165
|
+
return {
|
|
1166
|
+
targetNode: repository.getNode(input.targetNodeId),
|
|
1167
|
+
route: "explicit"
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const activeProjects = repository.listActiveNodesByType("project", 2);
|
|
1171
|
+
if (activeProjects.length === 1) {
|
|
1172
|
+
return {
|
|
1173
|
+
targetNode: activeProjects[0],
|
|
1174
|
+
route: "sole_active_project"
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
targetNode: repository.ensureWorkspaceInboxNode(),
|
|
1179
|
+
route: "workspace_inbox"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1163
1182
|
function createDurableNodeResponse(repository, input) {
|
|
1164
1183
|
const governance = resolveNodeGovernance(input, resolveGovernancePolicy(repository.getSettings(["review.autoApproveLowRisk", "review.trustedSourceToolNames"])));
|
|
1165
1184
|
const node = repository.createNode({
|
|
@@ -1204,6 +1223,7 @@ export function createRecallXApp(params) {
|
|
|
1204
1223
|
app.use((request, response, next) => {
|
|
1205
1224
|
const requestId = createId("req");
|
|
1206
1225
|
const traceId = request.header("x-recallx-trace-id")?.trim() || createId("trace");
|
|
1226
|
+
const parentSpanId = request.header("x-recallx-parent-span-id")?.trim() || null;
|
|
1207
1227
|
const operation = `${request.method.toUpperCase()} ${normalizeApiRequestPath(request.path)}`;
|
|
1208
1228
|
const observabilityState = currentObservabilityConfig();
|
|
1209
1229
|
const requestSpan = observability.startSpan({
|
|
@@ -1211,6 +1231,7 @@ export function createRecallXApp(params) {
|
|
|
1211
1231
|
operation,
|
|
1212
1232
|
requestId,
|
|
1213
1233
|
traceId,
|
|
1234
|
+
parentSpanId,
|
|
1214
1235
|
details: {
|
|
1215
1236
|
...(observabilityState.capturePayloadShape ? summarizePayloadShape(request.body) : {}),
|
|
1216
1237
|
mcpTool: request.header("x-recallx-mcp-tool") ?? null
|
|
@@ -1221,6 +1242,7 @@ export function createRecallXApp(params) {
|
|
|
1221
1242
|
response.locals.telemetryRequestSpan = requestSpan;
|
|
1222
1243
|
response.setHeader("x-recallx-request-id", requestId);
|
|
1223
1244
|
response.setHeader("x-recallx-trace-id", traceId);
|
|
1245
|
+
response.setHeader("x-recallx-span-id", requestSpan.spanId);
|
|
1224
1246
|
response.on("finish", () => {
|
|
1225
1247
|
void requestSpan.finish({
|
|
1226
1248
|
outcome: response.statusCode >= 400 ? "error" : "success",
|
|
@@ -1229,14 +1251,14 @@ export function createRecallXApp(params) {
|
|
|
1229
1251
|
errorKind: response.locals.telemetryErrorKind ?? null
|
|
1230
1252
|
});
|
|
1231
1253
|
});
|
|
1232
|
-
observability.withContext({
|
|
1254
|
+
requestSpan.run(() => observability.withContext({
|
|
1233
1255
|
traceId,
|
|
1234
1256
|
requestId,
|
|
1235
1257
|
workspaceRoot: currentWorkspaceRoot(),
|
|
1236
1258
|
workspaceName: currentWorkspaceInfo().workspaceName,
|
|
1237
1259
|
toolName: request.header("x-recallx-mcp-tool") ?? null,
|
|
1238
1260
|
surface: "api"
|
|
1239
|
-
}, next);
|
|
1261
|
+
}, next));
|
|
1240
1262
|
});
|
|
1241
1263
|
app.use(express.json({ limit: "2mb" }));
|
|
1242
1264
|
app.use("/api/v1", (request, response, next) => {
|
|
@@ -1597,7 +1619,8 @@ export function createRecallXApp(params) {
|
|
|
1597
1619
|
})()));
|
|
1598
1620
|
return;
|
|
1599
1621
|
}
|
|
1600
|
-
const
|
|
1622
|
+
const captureTarget = resolveCaptureActivityTarget(repository, input);
|
|
1623
|
+
const targetNode = captureTarget.targetNode;
|
|
1601
1624
|
const activity = repository.appendActivity({
|
|
1602
1625
|
targetNodeId: targetNode.id,
|
|
1603
1626
|
activityType: "agent_run_summary",
|
|
@@ -1605,6 +1628,7 @@ export function createRecallXApp(params) {
|
|
|
1605
1628
|
source,
|
|
1606
1629
|
metadata: {
|
|
1607
1630
|
...input.metadata,
|
|
1631
|
+
autoTargetRoute: captureTarget.route,
|
|
1608
1632
|
captureMode: input.mode,
|
|
1609
1633
|
capturedTitle: title
|
|
1610
1634
|
}
|
|
@@ -1635,9 +1659,13 @@ export function createRecallXApp(params) {
|
|
|
1635
1659
|
storedAs: "activity",
|
|
1636
1660
|
status: "recorded",
|
|
1637
1661
|
governanceState: null,
|
|
1638
|
-
reason:
|
|
1639
|
-
?
|
|
1640
|
-
|
|
1662
|
+
reason: captureTarget.route === "sole_active_project"
|
|
1663
|
+
? input.mode === "activity"
|
|
1664
|
+
? "Capture was routed to the sole active project timeline."
|
|
1665
|
+
: "Short log-like capture was routed to the sole active project timeline."
|
|
1666
|
+
: input.mode === "activity"
|
|
1667
|
+
? "Capture was explicitly routed to the activity timeline."
|
|
1668
|
+
: "Short log-like capture was routed to the activity timeline."
|
|
1641
1669
|
})
|
|
1642
1670
|
}));
|
|
1643
1671
|
});
|
|
@@ -1751,19 +1779,17 @@ export function createRecallXApp(params) {
|
|
|
1751
1779
|
}, (span) => {
|
|
1752
1780
|
const repository = currentRepository();
|
|
1753
1781
|
const nodeId = readRequestParam(request.params.id);
|
|
1754
|
-
const
|
|
1782
|
+
const neighborhoodOptions = {
|
|
1755
1783
|
relationTypes: types,
|
|
1756
1784
|
includeInferred,
|
|
1757
1785
|
maxInferred
|
|
1758
|
-
}
|
|
1786
|
+
};
|
|
1787
|
+
const result = buildNeighborhoodItems(repository, nodeId, neighborhoodOptions);
|
|
1759
1788
|
const expanded = depth === 2
|
|
1760
1789
|
? (() => {
|
|
1761
1790
|
const seen = new Set(result.map((item) => `${item.edge.relationId}:${item.node.id}:1`));
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
includeInferred,
|
|
1765
|
-
maxInferred
|
|
1766
|
-
})
|
|
1791
|
+
const secondHopByNodeId = buildNeighborhoodItemsBatch(repository, result.map((item) => item.node.id), neighborhoodOptions);
|
|
1792
|
+
const secondHop = result.flatMap((item) => (secondHopByNodeId.get(item.node.id) ?? [])
|
|
1767
1793
|
.filter((nested) => nested.node.id !== nodeId)
|
|
1768
1794
|
.map((nested) => ({
|
|
1769
1795
|
...nested,
|
|
@@ -1786,7 +1812,8 @@ export function createRecallXApp(params) {
|
|
|
1786
1812
|
})()
|
|
1787
1813
|
: result;
|
|
1788
1814
|
span.addDetails({
|
|
1789
|
-
resultCount: expanded.length
|
|
1815
|
+
resultCount: expanded.length,
|
|
1816
|
+
firstHopCount: result.length
|
|
1790
1817
|
});
|
|
1791
1818
|
return expanded;
|
|
1792
1819
|
});
|
|
@@ -2126,8 +2153,9 @@ export function createRecallXApp(params) {
|
|
|
2126
2153
|
});
|
|
2127
2154
|
const semanticAugmentation = repository.getSemanticAugmentationSettings();
|
|
2128
2155
|
const semanticEnabled = shouldUseSemanticCandidateAugmentation(query, candidates);
|
|
2156
|
+
const semanticCandidateIds = selectSemanticCandidateIds(query, candidates);
|
|
2129
2157
|
const semanticBonuses = semanticEnabled
|
|
2130
|
-
? buildSemanticCandidateBonusMap(await repository.rankSemanticCandidates(query,
|
|
2158
|
+
? buildSemanticCandidateBonusMap(await repository.rankSemanticCandidates(query, semanticCandidateIds), semanticAugmentation)
|
|
2131
2159
|
: new Map();
|
|
2132
2160
|
const result = candidates
|
|
2133
2161
|
.map((node) => {
|
|
@@ -2151,7 +2179,9 @@ export function createRecallXApp(params) {
|
|
|
2151
2179
|
.sort((left, right) => right.score - left.score);
|
|
2152
2180
|
span.addDetails({
|
|
2153
2181
|
resultCount: result.length,
|
|
2154
|
-
semanticUsed: semanticEnabled
|
|
2182
|
+
semanticUsed: semanticEnabled,
|
|
2183
|
+
semanticCandidateCount: candidates.length,
|
|
2184
|
+
semanticRankedCandidateCount: semanticCandidateIds.length
|
|
2155
2185
|
});
|
|
2156
2186
|
return result;
|
|
2157
2187
|
});
|
package/app/server/governance.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AppError } from "./errors.js";
|
|
2
2
|
import { countTokensApprox, nowIso } from "./utils.js";
|
|
3
|
-
const relaxedShortFormNodeTypes = new Set(["reference", "question", "conversation"]);
|
|
3
|
+
const relaxedShortFormNodeTypes = new Set(["project", "reference", "question", "conversation"]);
|
|
4
4
|
function clampConfidence(value) {
|
|
5
5
|
return Math.min(Math.max(value, 0), 1);
|
|
6
6
|
}
|
|
@@ -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,16 +1,41 @@
|
|
|
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";
|
|
6
|
+
import { createId } from "./utils.js";
|
|
4
7
|
const telemetryStorage = new AsyncLocalStorage();
|
|
5
8
|
function nowIso() {
|
|
6
9
|
return new Date().toISOString();
|
|
7
10
|
}
|
|
11
|
+
function createSpanId() {
|
|
12
|
+
return createId("span");
|
|
13
|
+
}
|
|
8
14
|
function parseJsonLine(line) {
|
|
9
15
|
if (!line.trim()) {
|
|
10
16
|
return null;
|
|
11
17
|
}
|
|
12
18
|
try {
|
|
13
|
-
|
|
19
|
+
const parsed = JSON.parse(line);
|
|
20
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.ts !== "string" || typeof parsed.operation !== "string") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ts: parsed.ts,
|
|
25
|
+
traceId: typeof parsed.traceId === "string" ? parsed.traceId : "trace_unknown",
|
|
26
|
+
spanId: typeof parsed.spanId === "string" ? parsed.spanId : null,
|
|
27
|
+
parentSpanId: typeof parsed.parentSpanId === "string" ? parsed.parentSpanId : null,
|
|
28
|
+
requestId: typeof parsed.requestId === "string" ? parsed.requestId : null,
|
|
29
|
+
surface: parsed.surface === "mcp" ? "mcp" : "api",
|
|
30
|
+
operation: parsed.operation,
|
|
31
|
+
outcome: parsed.outcome === "error" ? "error" : "success",
|
|
32
|
+
durationMs: typeof parsed.durationMs === "number" ? parsed.durationMs : null,
|
|
33
|
+
statusCode: typeof parsed.statusCode === "number" ? parsed.statusCode : null,
|
|
34
|
+
errorCode: typeof parsed.errorCode === "string" ? parsed.errorCode : null,
|
|
35
|
+
errorKind: typeof parsed.errorKind === "string" ? parsed.errorKind : null,
|
|
36
|
+
workspaceName: typeof parsed.workspaceName === "string" ? parsed.workspaceName : null,
|
|
37
|
+
details: parsed.details && typeof parsed.details === "object" && !Array.isArray(parsed.details) ? parsed.details : {}
|
|
38
|
+
};
|
|
14
39
|
}
|
|
15
40
|
catch {
|
|
16
41
|
return null;
|
|
@@ -22,6 +47,10 @@ function roundDuration(value) {
|
|
|
22
47
|
function dateStamp(value) {
|
|
23
48
|
return value.slice(0, 10);
|
|
24
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
|
+
}
|
|
25
54
|
function normalizeRetentionDays(value) {
|
|
26
55
|
return Math.max(1, Math.trunc(value || 14));
|
|
27
56
|
}
|
|
@@ -142,14 +171,18 @@ export class TelemetrySpan {
|
|
|
142
171
|
writer;
|
|
143
172
|
state;
|
|
144
173
|
context;
|
|
174
|
+
spanId;
|
|
175
|
+
parentSpanId;
|
|
145
176
|
operation;
|
|
146
177
|
startedAt = process.hrtime.bigint();
|
|
147
178
|
details;
|
|
148
179
|
finished = false;
|
|
149
|
-
constructor(writer, state, context, operation, details) {
|
|
180
|
+
constructor(writer, state, context, spanId, parentSpanId, operation, details) {
|
|
150
181
|
this.writer = writer;
|
|
151
182
|
this.state = state;
|
|
152
183
|
this.context = context;
|
|
184
|
+
this.spanId = spanId;
|
|
185
|
+
this.parentSpanId = parentSpanId;
|
|
153
186
|
this.operation = operation;
|
|
154
187
|
this.details = sanitizeDetails(details);
|
|
155
188
|
}
|
|
@@ -165,6 +198,8 @@ export class TelemetrySpan {
|
|
|
165
198
|
await this.writer.enqueue({
|
|
166
199
|
ts: nowIso(),
|
|
167
200
|
traceId: this.context.traceId,
|
|
201
|
+
spanId: this.spanId,
|
|
202
|
+
parentSpanId: this.parentSpanId,
|
|
168
203
|
requestId: input.requestId ?? this.context.requestId,
|
|
169
204
|
surface: this.context.surface,
|
|
170
205
|
operation: this.operation,
|
|
@@ -198,14 +233,16 @@ export class ObservabilityWriter {
|
|
|
198
233
|
return telemetryStorage.getStore() ?? null;
|
|
199
234
|
}
|
|
200
235
|
withContext(input, callback) {
|
|
236
|
+
const current = telemetryStorage.getStore();
|
|
201
237
|
return telemetryStorage.run({
|
|
202
238
|
...input,
|
|
203
|
-
spans: []
|
|
239
|
+
spans: current?.spans ?? []
|
|
204
240
|
}, callback);
|
|
205
241
|
}
|
|
206
242
|
startSpan(input) {
|
|
207
243
|
const state = this.options.getState();
|
|
208
244
|
const current = telemetryStorage.getStore();
|
|
245
|
+
const parentSpan = current?.spans[current.spans.length - 1] ?? null;
|
|
209
246
|
const context = {
|
|
210
247
|
traceId: input.traceId ?? current?.traceId ?? "trace_unknown",
|
|
211
248
|
requestId: input.requestId ?? current?.requestId ?? null,
|
|
@@ -215,7 +252,7 @@ export class ObservabilityWriter {
|
|
|
215
252
|
toolName: current?.toolName ?? null,
|
|
216
253
|
spans: current?.spans ?? []
|
|
217
254
|
};
|
|
218
|
-
return new TelemetrySpan(this, state, context, input.operation, input.details);
|
|
255
|
+
return new TelemetrySpan(this, state, context, createSpanId(), input.parentSpanId ?? parentSpan?.spanId ?? null, input.operation, input.details);
|
|
219
256
|
}
|
|
220
257
|
addCurrentSpanDetails(details) {
|
|
221
258
|
const current = telemetryStorage.getStore();
|
|
@@ -230,6 +267,8 @@ export class ObservabilityWriter {
|
|
|
230
267
|
await this.enqueue({
|
|
231
268
|
ts: nowIso(),
|
|
232
269
|
traceId: input.traceId ?? current?.traceId ?? "trace_unknown",
|
|
270
|
+
spanId: createSpanId(),
|
|
271
|
+
parentSpanId: current?.spans[current.spans.length - 1]?.spanId ?? null,
|
|
233
272
|
requestId: input.requestId ?? current?.requestId ?? null,
|
|
234
273
|
surface: input.surface ?? current?.surface ?? "api",
|
|
235
274
|
operation: input.operation,
|
|
@@ -308,24 +347,41 @@ export class ObservabilityWriter {
|
|
|
308
347
|
}
|
|
309
348
|
const files = entries
|
|
310
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
|
+
})
|
|
311
354
|
.sort();
|
|
312
355
|
const events = [];
|
|
313
356
|
for (const file of files) {
|
|
314
357
|
const filePath = path.join(logsDir, file);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
377
|
}
|
|
328
|
-
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Ignore individual file read failures so observability endpoints stay resilient.
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
lines.close();
|
|
384
|
+
stream.destroy();
|
|
329
385
|
}
|
|
330
386
|
}
|
|
331
387
|
return {
|
|
@@ -497,9 +553,10 @@ export class ObservabilityWriter {
|
|
|
497
553
|
}
|
|
498
554
|
workspaceFallbackModeBuckets.set(bucketKey, current);
|
|
499
555
|
}
|
|
500
|
-
if (event.
|
|
501
|
-
event.details.feedbackVerdict === "
|
|
502
|
-
|
|
556
|
+
if (event.operation === "search.feedback" &&
|
|
557
|
+
(event.details.feedbackVerdict === "useful" ||
|
|
558
|
+
event.details.feedbackVerdict === "not_useful" ||
|
|
559
|
+
event.details.feedbackVerdict === "uncertain")) {
|
|
503
560
|
feedbackSampleCount += 1;
|
|
504
561
|
if (event.details.feedbackVerdict === "useful") {
|
|
505
562
|
feedbackUsefulCount += 1;
|
|
@@ -863,6 +920,10 @@ export function createObservabilityWriter(options) {
|
|
|
863
920
|
export function currentTelemetryContext() {
|
|
864
921
|
return telemetryStorage.getStore() ?? null;
|
|
865
922
|
}
|
|
923
|
+
export function currentTelemetrySpanId() {
|
|
924
|
+
const current = telemetryStorage.getStore();
|
|
925
|
+
return current?.spans[current.spans.length - 1]?.spanId ?? null;
|
|
926
|
+
}
|
|
866
927
|
export function appendCurrentTelemetryDetails(details) {
|
|
867
928
|
const current = telemetryStorage.getStore();
|
|
868
929
|
current?.spans[current.spans.length - 1]?.addDetails(details);
|
|
@@ -1282,6 +1282,17 @@ export class RecallXRepository {
|
|
|
1282
1282
|
tags: parseJson(row.tags_json, [])
|
|
1283
1283
|
}));
|
|
1284
1284
|
}
|
|
1285
|
+
listActiveNodesByType(type, limit = 20) {
|
|
1286
|
+
const rows = this.db
|
|
1287
|
+
.prepare(`SELECT *
|
|
1288
|
+
FROM nodes
|
|
1289
|
+
WHERE type = ?
|
|
1290
|
+
AND status = 'active'
|
|
1291
|
+
ORDER BY updated_at DESC, id DESC
|
|
1292
|
+
LIMIT ?`)
|
|
1293
|
+
.all(type, limit);
|
|
1294
|
+
return rows.map(mapNode);
|
|
1295
|
+
}
|
|
1285
1296
|
listInferenceCandidateNodes(targetNodeId, limit = 200) {
|
|
1286
1297
|
const rows = this.db
|
|
1287
1298
|
.prepare(`SELECT * FROM nodes
|