@rubytech/create-realagent 1.0.693 → 1.0.696
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/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +127 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/index.js +393 -0
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-search/src/__tests__/bm25-only.test.ts +129 -0
- package/payload/platform/lib/graph-search/src/__tests__/escape-and-normalise.test.ts +53 -0
- package/payload/platform/lib/graph-search/src/__tests__/hybrid.test.ts +190 -0
- package/payload/platform/lib/graph-search/src/index.ts +498 -0
- package/payload/platform/lib/graph-search/tsconfig.json +9 -0
- package/payload/platform/lib/graph-search/vitest.config.ts +9 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +61 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.js +97 -0
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-write/src/index.ts +167 -0
- package/payload/platform/lib/graph-write/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +19 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +58 -0
- package/payload/platform/plugins/admin/skills/unzip-attachment/references/safety.md +81 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +27 -3
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +4 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +10 -6
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +43 -36
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/docs/references/attachments.md +44 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +44 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +3 -32
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +18 -381
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +9 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -23
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +8 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +24 -10
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +8 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +2 -0
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -18
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +12 -2
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
- package/payload/server/chunk-IAIGB5WN.js +11406 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-zbb1g-mh.js → admin-BZSstsyc.js} +60 -60
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +660 -22
package/payload/server/server.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE,
|
|
8
8
|
TELEGRAM_WEBHOOK_SECRET_FILE,
|
|
9
9
|
USERS_FILE,
|
|
10
|
+
__commonJS,
|
|
11
|
+
__toESM,
|
|
10
12
|
agentLogStream,
|
|
11
13
|
backfillNullUserIdConversations,
|
|
12
14
|
bindVisitorToGroup,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
computeConstraints,
|
|
23
25
|
createRemoteSession,
|
|
24
26
|
deleteConversation,
|
|
27
|
+
embed,
|
|
25
28
|
ensureAuth,
|
|
26
29
|
ensureCdp,
|
|
27
30
|
ensureLogDir,
|
|
@@ -74,7 +77,6 @@ import {
|
|
|
74
77
|
resolveUserAccounts,
|
|
75
78
|
safeJson,
|
|
76
79
|
sanitizeClientCorrId,
|
|
77
|
-
searchKnowledgeFulltext,
|
|
78
80
|
seedSessionHistory,
|
|
79
81
|
serve,
|
|
80
82
|
setConversationIdForSession,
|
|
@@ -95,7 +97,281 @@ import {
|
|
|
95
97
|
vncLog,
|
|
96
98
|
waitForExit,
|
|
97
99
|
writeChromiumWrapper
|
|
98
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-IAIGB5WN.js";
|
|
101
|
+
|
|
102
|
+
// ../lib/graph-trash/dist/index.js
|
|
103
|
+
var require_dist = __commonJS({
|
|
104
|
+
"../lib/graph-trash/dist/index.js"(exports) {
|
|
105
|
+
"use strict";
|
|
106
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
107
|
+
exports.TRASH_METADATA_PROPS = void 0;
|
|
108
|
+
exports.trashNode = trashNode2;
|
|
109
|
+
exports.restoreNode = restoreNode2;
|
|
110
|
+
exports.emptyTrash = emptyTrash;
|
|
111
|
+
exports.notTrashed = notTrashed2;
|
|
112
|
+
exports.uniqueKeysForLabels = uniqueKeysForLabels;
|
|
113
|
+
var UNIQUE_KEYS_BY_LABEL2 = {
|
|
114
|
+
Person: ["email", "telephone"],
|
|
115
|
+
Service: ["serviceId"],
|
|
116
|
+
LocalBusiness: ["accountId"],
|
|
117
|
+
Task: ["taskId"],
|
|
118
|
+
Event: ["eventId"],
|
|
119
|
+
KnowledgeDocument: ["attachmentId"],
|
|
120
|
+
DigitalDocument: ["attachmentId"],
|
|
121
|
+
Conversation: ["conversationId", "sessionKey"],
|
|
122
|
+
Message: ["messageId"],
|
|
123
|
+
OnboardingState: ["accountId"],
|
|
124
|
+
Workflow: ["workflowId"],
|
|
125
|
+
WorkflowStep: ["stepId"],
|
|
126
|
+
WorkflowRun: ["runId"],
|
|
127
|
+
Preference: ["preferenceId"],
|
|
128
|
+
Email: ["emailId", "messageId"],
|
|
129
|
+
AdminUser: ["userId"],
|
|
130
|
+
ToolCall: ["callId"],
|
|
131
|
+
// Composite component nulls — frees the composite constraint:
|
|
132
|
+
AccessGrant: ["contactValue"],
|
|
133
|
+
// composite (contactValue, agentSlug, accountId)
|
|
134
|
+
UserProfile: ["userId"]
|
|
135
|
+
// composite (accountId, userId)
|
|
136
|
+
};
|
|
137
|
+
var TRASH_PROP_NAMES = [
|
|
138
|
+
"trashedAt",
|
|
139
|
+
"trashedBy",
|
|
140
|
+
"trashReason",
|
|
141
|
+
"_trashedKeys"
|
|
142
|
+
];
|
|
143
|
+
async function trashNode2(params) {
|
|
144
|
+
const { session, accountId, elementId, by, reason } = params;
|
|
145
|
+
const lookup = await session.run(`MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId
|
|
146
|
+
RETURN labels(n) AS labels, properties(n) AS props`, { eid: elementId, accountId });
|
|
147
|
+
if (lookup.records.length === 0) {
|
|
148
|
+
throw new Error(`trashNode: node not found (elementId=${elementId} accountId=${accountId.slice(0, 8)}\u2026)`);
|
|
149
|
+
}
|
|
150
|
+
const allLabels = lookup.records[0].get("labels");
|
|
151
|
+
const props = lookup.records[0].get("props");
|
|
152
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
153
|
+
if (allLabels.includes("Trashed")) {
|
|
154
|
+
return {
|
|
155
|
+
trashed: false,
|
|
156
|
+
alreadyTrashed: true,
|
|
157
|
+
nodeId: elementId,
|
|
158
|
+
labels: baseLabels,
|
|
159
|
+
trashedAt: String(props.trashedAt ?? ""),
|
|
160
|
+
originalKeys: {}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const label of baseLabels) {
|
|
165
|
+
for (const key of UNIQUE_KEYS_BY_LABEL2[label] ?? [])
|
|
166
|
+
uniqueKeys.add(key);
|
|
167
|
+
}
|
|
168
|
+
const originalKeys = {};
|
|
169
|
+
for (const k of uniqueKeys) {
|
|
170
|
+
if (props[k] !== void 0 && props[k] !== null)
|
|
171
|
+
originalKeys[k] = props[k];
|
|
172
|
+
}
|
|
173
|
+
const setNullClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = null`).join(", ");
|
|
174
|
+
const setNullSuffix = setNullClauses ? `, ${setNullClauses}` : "";
|
|
175
|
+
const trashedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
176
|
+
const isConversation = baseLabels.includes("Conversation");
|
|
177
|
+
const messageUniqueKeys = UNIQUE_KEYS_BY_LABEL2["Message"] ?? [];
|
|
178
|
+
let cascadedMessageCount = 0;
|
|
179
|
+
await session.executeWrite(async (tx) => {
|
|
180
|
+
await tx.run(`MATCH (n) WHERE elementId(n) = $eid
|
|
181
|
+
SET n:Trashed,
|
|
182
|
+
n.trashedAt = datetime($trashedAt),
|
|
183
|
+
n.trashedBy = $by,
|
|
184
|
+
n.trashReason = $reason,
|
|
185
|
+
n._trashedKeys = $trashedKeysJson${setNullSuffix}`, {
|
|
186
|
+
eid: elementId,
|
|
187
|
+
trashedAt,
|
|
188
|
+
by,
|
|
189
|
+
reason: reason ?? null,
|
|
190
|
+
trashedKeysJson: JSON.stringify(originalKeys)
|
|
191
|
+
});
|
|
192
|
+
if (isConversation) {
|
|
193
|
+
const collectKeys = messageUniqueKeys.length > 0 ? messageUniqueKeys.map((k) => `\`${k}\`: m.\`${k}\``).join(", ") : "";
|
|
194
|
+
const collectMsgProps = collectKeys ? `, {${collectKeys}}` : ", {}";
|
|
195
|
+
const collected = await tx.run(`MATCH (c) WHERE elementId(c) = $eid
|
|
196
|
+
MATCH (m:Message)-[:PART_OF]->(c)
|
|
197
|
+
WHERE NOT m:Trashed
|
|
198
|
+
RETURN elementId(m) AS meid${collectMsgProps} AS keys`, { eid: elementId });
|
|
199
|
+
for (const rec of collected.records) {
|
|
200
|
+
const meid = rec.get("meid");
|
|
201
|
+
const keys = rec.get("keys");
|
|
202
|
+
const liveKeys = {};
|
|
203
|
+
for (const k of messageUniqueKeys) {
|
|
204
|
+
if (keys[k] !== void 0 && keys[k] !== null)
|
|
205
|
+
liveKeys[k] = keys[k];
|
|
206
|
+
}
|
|
207
|
+
const msgSetNulls = Object.keys(liveKeys).length > 0 ? ", " + Object.keys(liveKeys).map((k) => `m.\`${k}\` = null`).join(", ") : "";
|
|
208
|
+
await tx.run(`MATCH (m) WHERE elementId(m) = $meid
|
|
209
|
+
SET m:Trashed,
|
|
210
|
+
m.trashedAt = datetime($trashedAt),
|
|
211
|
+
m.trashedBy = $by,
|
|
212
|
+
m.trashReason = $reason,
|
|
213
|
+
m._trashedKeys = $trashedKeysJson${msgSetNulls}`, {
|
|
214
|
+
meid,
|
|
215
|
+
trashedAt,
|
|
216
|
+
by: `${by}:cascade-from-conversation`,
|
|
217
|
+
reason: reason ?? `cascade from Conversation ${elementId}`,
|
|
218
|
+
trashedKeysJson: JSON.stringify(liveKeys)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
cascadedMessageCount = collected.records.length;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
process.stderr.write(`[trash:marked] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")} by=${by} reason=${reason ?? "null"}
|
|
225
|
+
`);
|
|
226
|
+
if (isConversation) {
|
|
227
|
+
process.stderr.write(`[trash:cascaded] accountId=${accountId} conversationElementId=${elementId} messageCount=${cascadedMessageCount} by=${by}
|
|
228
|
+
`);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
trashed: true,
|
|
232
|
+
alreadyTrashed: false,
|
|
233
|
+
nodeId: elementId,
|
|
234
|
+
labels: baseLabels,
|
|
235
|
+
trashedAt,
|
|
236
|
+
originalKeys
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function restoreNode2(params) {
|
|
240
|
+
const { session, accountId, elementId } = params;
|
|
241
|
+
const lookup = await session.run(`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
242
|
+
RETURN labels(n) AS labels, n._trashedKeys AS keysJson`, { eid: elementId });
|
|
243
|
+
if (lookup.records.length === 0) {
|
|
244
|
+
throw new Error(`restoreNode: trashed node not found (elementId=${elementId})`);
|
|
245
|
+
}
|
|
246
|
+
const allLabels = lookup.records[0].get("labels");
|
|
247
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
248
|
+
const keysJson = lookup.records[0].get("keysJson");
|
|
249
|
+
const originalKeys = keysJson ? JSON.parse(keysJson) : {};
|
|
250
|
+
for (const label of baseLabels) {
|
|
251
|
+
const uniqueKeys = UNIQUE_KEYS_BY_LABEL2[label] ?? [];
|
|
252
|
+
for (const k of uniqueKeys) {
|
|
253
|
+
const v = originalKeys[k];
|
|
254
|
+
if (v === void 0 || v === null)
|
|
255
|
+
continue;
|
|
256
|
+
const conflict = await session.run(`MATCH (other:\`${label}\`)
|
|
257
|
+
WHERE elementId(other) <> $eid
|
|
258
|
+
AND NOT other:Trashed
|
|
259
|
+
AND other.\`${k}\` = $val
|
|
260
|
+
RETURN elementId(other) AS otherId LIMIT 1`, { eid: elementId, val: v });
|
|
261
|
+
if (conflict.records.length > 0) {
|
|
262
|
+
const otherId = conflict.records[0].get("otherId");
|
|
263
|
+
throw new Error(`restoreNode: cannot restore ${label} elementId=${elementId} \u2014 active node elementId=${otherId} already holds ${k}=${JSON.stringify(v)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const setClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = $val_${k}`).join(", ");
|
|
268
|
+
const setSuffix = setClauses ? `, ${setClauses}` : "";
|
|
269
|
+
const setParams = { eid: elementId };
|
|
270
|
+
for (const [k, v] of Object.entries(originalKeys))
|
|
271
|
+
setParams[`val_${k}`] = v;
|
|
272
|
+
const isConversation = baseLabels.includes("Conversation");
|
|
273
|
+
let cascadedMessageCount = 0;
|
|
274
|
+
await session.executeWrite(async (tx) => {
|
|
275
|
+
await tx.run(`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
276
|
+
REMOVE n:Trashed, n.trashedAt, n.trashedBy, n.trashReason, n._trashedKeys
|
|
277
|
+
SET n.restoredAt = datetime()${setSuffix}`, setParams);
|
|
278
|
+
if (isConversation) {
|
|
279
|
+
const collected = await tx.run(`MATCH (c) WHERE elementId(c) = $eid
|
|
280
|
+
MATCH (m:Trashed:Message)-[:PART_OF]->(c)
|
|
281
|
+
WHERE m.trashedBy ENDS WITH ':cascade-from-conversation'
|
|
282
|
+
RETURN elementId(m) AS meid, m._trashedKeys AS keysJson`, { eid: elementId });
|
|
283
|
+
for (const rec of collected.records) {
|
|
284
|
+
const meid = rec.get("meid");
|
|
285
|
+
const keysJson2 = rec.get("keysJson");
|
|
286
|
+
const keys = keysJson2 ? JSON.parse(keysJson2) : {};
|
|
287
|
+
const setClause = Object.keys(keys).length > 0 ? ", " + Object.keys(keys).map((k) => `m.\`${k}\` = $val_${k}`).join(", ") : "";
|
|
288
|
+
const msgParams = { meid };
|
|
289
|
+
for (const [k, v] of Object.entries(keys))
|
|
290
|
+
msgParams[`val_${k}`] = v;
|
|
291
|
+
await tx.run(`MATCH (m) WHERE elementId(m) = $meid
|
|
292
|
+
REMOVE m:Trashed, m.trashedAt, m.trashedBy, m.trashReason, m._trashedKeys
|
|
293
|
+
SET m.restoredAt = datetime()${setClause}`, msgParams);
|
|
294
|
+
}
|
|
295
|
+
cascadedMessageCount = collected.records.length;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
process.stderr.write(`[trash:restored] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")}
|
|
299
|
+
`);
|
|
300
|
+
if (isConversation) {
|
|
301
|
+
process.stderr.write(`[trash:cascade-restored] accountId=${accountId} conversationElementId=${elementId} messageCount=${cascadedMessageCount}
|
|
302
|
+
`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
restored: true,
|
|
306
|
+
nodeId: elementId,
|
|
307
|
+
labels: baseLabels,
|
|
308
|
+
restoredKeys: originalKeys
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async function emptyTrash(params) {
|
|
312
|
+
const t0 = Date.now();
|
|
313
|
+
const { session, accountId, graceDays = 30, dryRun = false, labels: labelFilter, onEmpty } = params;
|
|
314
|
+
const cutoff = new Date(Date.now() - graceDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
315
|
+
const labelClause = labelFilter && labelFilter.length > 0 ? `AND ANY(l IN labels(n) WHERE l IN $labelFilter)` : "";
|
|
316
|
+
const candidatesResult = await session.run(`MATCH (n:Trashed)
|
|
317
|
+
WHERE n.trashedAt < datetime($cutoff)
|
|
318
|
+
${labelClause}
|
|
319
|
+
RETURN elementId(n) AS eid,
|
|
320
|
+
labels(n) AS labels,
|
|
321
|
+
toString(n.trashedAt) AS trashedAt,
|
|
322
|
+
n.trashedBy AS trashedBy,
|
|
323
|
+
n._trashedKeys AS keysJson
|
|
324
|
+
ORDER BY n.trashedAt ASC`, { cutoff, ...labelFilter ? { labelFilter } : {} });
|
|
325
|
+
const candidates = candidatesResult.records.map((r) => {
|
|
326
|
+
const keysJson = r.get("keysJson");
|
|
327
|
+
return {
|
|
328
|
+
elementId: r.get("eid"),
|
|
329
|
+
labels: r.get("labels").filter((l) => l !== "Trashed"),
|
|
330
|
+
trashedAt: String(r.get("trashedAt")),
|
|
331
|
+
trashedBy: r.get("trashedBy") ?? null,
|
|
332
|
+
trashedKeys: keysJson ? JSON.parse(keysJson) : {}
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
process.stderr.write(`[trash:empty-run] accountId=${accountId} graceDays=${graceDays} dryRun=${dryRun} candidates=${candidates.length}
|
|
336
|
+
`);
|
|
337
|
+
if (dryRun || candidates.length === 0) {
|
|
338
|
+
process.stderr.write(`[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=0 graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}
|
|
339
|
+
`);
|
|
340
|
+
return { graceDays, dryRun, candidates, emptied: 0 };
|
|
341
|
+
}
|
|
342
|
+
let emptied = 0;
|
|
343
|
+
for (const c of candidates) {
|
|
344
|
+
if (onEmpty) {
|
|
345
|
+
try {
|
|
346
|
+
await onEmpty(c);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
process.stderr.write(`[trash:empty-run] onEmpty side-effect failed for elementId=${c.elementId}: ${err instanceof Error ? err.message : String(err)}
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
await session.run(`MATCH (n) WHERE elementId(n) = $eid DETACH DELETE n`, { eid: c.elementId });
|
|
353
|
+
const ageDays = Math.floor((Date.now() - new Date(c.trashedAt).getTime()) / (24 * 60 * 60 * 1e3));
|
|
354
|
+
process.stderr.write(`[trash:emptied] accountId=${accountId} elementId=${c.elementId} labels=${c.labels.join(",")} trashedAt=${c.trashedAt} ageDays=${ageDays}
|
|
355
|
+
`);
|
|
356
|
+
emptied++;
|
|
357
|
+
}
|
|
358
|
+
process.stderr.write(`[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=${emptied} graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}
|
|
359
|
+
`);
|
|
360
|
+
return { graceDays, dryRun, candidates, emptied };
|
|
361
|
+
}
|
|
362
|
+
function notTrashed2(alias) {
|
|
363
|
+
return `(NOT \`${alias}\`:Trashed AND \`${alias}\`.deletedAt IS NULL)`;
|
|
364
|
+
}
|
|
365
|
+
function uniqueKeysForLabels(labels) {
|
|
366
|
+
const out = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const l of labels)
|
|
368
|
+
for (const k of UNIQUE_KEYS_BY_LABEL2[l] ?? [])
|
|
369
|
+
out.add(k);
|
|
370
|
+
return [...out];
|
|
371
|
+
}
|
|
372
|
+
exports.TRASH_METADATA_PROPS = TRASH_PROP_NAMES;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
99
375
|
|
|
100
376
|
// node_modules/hono/dist/utils/mime.js
|
|
101
377
|
var getMimeType = (filename, mimes = baseMimes) => {
|
|
@@ -2405,12 +2681,12 @@ function createBaileysLogger(bindings = {}) {
|
|
|
2405
2681
|
var credsSaveQueue = Promise.resolve();
|
|
2406
2682
|
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
2407
2683
|
console.error(`${TAG3} draining credential save queue\u2026`);
|
|
2408
|
-
const
|
|
2684
|
+
const timer2 = new Promise(
|
|
2409
2685
|
(resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
|
|
2410
2686
|
);
|
|
2411
2687
|
const result = await Promise.race([
|
|
2412
2688
|
credsSaveQueue.then(() => "drained"),
|
|
2413
|
-
|
|
2689
|
+
timer2
|
|
2414
2690
|
]);
|
|
2415
2691
|
if (result === "timeout") {
|
|
2416
2692
|
console.error(`${TAG3} credential save queue drain timed out after ${timeoutMs}ms`);
|
|
@@ -2653,16 +2929,16 @@ ${inspected}`;
|
|
|
2653
2929
|
}
|
|
2654
2930
|
function withTimeout(label, promise, timeoutMs) {
|
|
2655
2931
|
return new Promise((resolve22, reject) => {
|
|
2656
|
-
const
|
|
2932
|
+
const timer2 = setTimeout(() => {
|
|
2657
2933
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
2658
2934
|
}, timeoutMs);
|
|
2659
2935
|
promise.then(
|
|
2660
2936
|
(value) => {
|
|
2661
|
-
clearTimeout(
|
|
2937
|
+
clearTimeout(timer2);
|
|
2662
2938
|
resolve22(value);
|
|
2663
2939
|
},
|
|
2664
2940
|
(err) => {
|
|
2665
|
-
clearTimeout(
|
|
2941
|
+
clearTimeout(timer2);
|
|
2666
2942
|
reject(err);
|
|
2667
2943
|
}
|
|
2668
2944
|
);
|
|
@@ -3350,10 +3626,10 @@ function createInboundDebouncer(opts) {
|
|
|
3350
3626
|
flushKey(key).catch(onError);
|
|
3351
3627
|
}, debounceMs);
|
|
3352
3628
|
} else {
|
|
3353
|
-
const
|
|
3629
|
+
const timer2 = setTimeout(() => {
|
|
3354
3630
|
flushKey(key).catch(onError);
|
|
3355
3631
|
}, debounceMs);
|
|
3356
|
-
pending.set(key, { entries: [msg], timer });
|
|
3632
|
+
pending.set(key, { entries: [msg], timer: timer2 });
|
|
3357
3633
|
}
|
|
3358
3634
|
}
|
|
3359
3635
|
function registerPending(key, promise) {
|
|
@@ -3874,9 +4150,9 @@ async function connectWithReconnect(conn) {
|
|
|
3874
4150
|
`${TAG11} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
3875
4151
|
);
|
|
3876
4152
|
await new Promise((resolve22) => {
|
|
3877
|
-
const
|
|
4153
|
+
const timer2 = setTimeout(resolve22, delay);
|
|
3878
4154
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
3879
|
-
clearTimeout(
|
|
4155
|
+
clearTimeout(timer2);
|
|
3880
4156
|
resolve22();
|
|
3881
4157
|
}, { once: true });
|
|
3882
4158
|
});
|
|
@@ -4671,10 +4947,13 @@ var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
4671
4947
|
"text/markdown",
|
|
4672
4948
|
"text/csv",
|
|
4673
4949
|
"text/html",
|
|
4674
|
-
"text/calendar"
|
|
4950
|
+
"text/calendar",
|
|
4951
|
+
"application/zip",
|
|
4952
|
+
"application/x-zip-compressed"
|
|
4675
4953
|
]);
|
|
4676
4954
|
var MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
4677
4955
|
var MAX_FILES_PER_MESSAGE = 5;
|
|
4956
|
+
var MAX_ZIP_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
4678
4957
|
function assertSupportedMime(mimeType) {
|
|
4679
4958
|
if (!SUPPORTED_MIME_TYPES.has(mimeType)) {
|
|
4680
4959
|
throw new Error(
|
|
@@ -5013,7 +5292,7 @@ async function processInbound(rawText, channel) {
|
|
|
5013
5292
|
return result;
|
|
5014
5293
|
}
|
|
5015
5294
|
const controller = new AbortController();
|
|
5016
|
-
const
|
|
5295
|
+
const timer2 = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
|
|
5017
5296
|
try {
|
|
5018
5297
|
const client = new Anthropic({ apiKey });
|
|
5019
5298
|
const response = await client.messages.create(
|
|
@@ -5089,7 +5368,7 @@ async function processInbound(rawText, channel) {
|
|
|
5089
5368
|
);
|
|
5090
5369
|
return defaultResult(rawText.trim(), latencyMs);
|
|
5091
5370
|
} finally {
|
|
5092
|
-
clearTimeout(
|
|
5371
|
+
clearTimeout(timer2);
|
|
5093
5372
|
}
|
|
5094
5373
|
}
|
|
5095
5374
|
|
|
@@ -7560,7 +7839,7 @@ function startScriptStreamTailer(opts) {
|
|
|
7560
7839
|
let buffer = "";
|
|
7561
7840
|
let stopped = false;
|
|
7562
7841
|
let pendingRead = false;
|
|
7563
|
-
let
|
|
7842
|
+
let timer2;
|
|
7564
7843
|
const processLine = (line) => {
|
|
7565
7844
|
const event = parseLine(line);
|
|
7566
7845
|
if (event) onEvent(event);
|
|
@@ -7607,15 +7886,15 @@ function startScriptStreamTailer(opts) {
|
|
|
7607
7886
|
if (stopped) return;
|
|
7608
7887
|
readDelta().catch(() => {
|
|
7609
7888
|
}).finally(() => {
|
|
7610
|
-
if (!stopped)
|
|
7889
|
+
if (!stopped) timer2 = setTimeout(tick, 200);
|
|
7611
7890
|
});
|
|
7612
7891
|
};
|
|
7613
|
-
|
|
7892
|
+
timer2 = setTimeout(tick, 0);
|
|
7614
7893
|
return {
|
|
7615
7894
|
async stop() {
|
|
7616
7895
|
if (stopped) return;
|
|
7617
7896
|
stopped = true;
|
|
7618
|
-
if (
|
|
7897
|
+
if (timer2) clearTimeout(timer2);
|
|
7619
7898
|
while (pendingRead) {
|
|
7620
7899
|
await new Promise((r) => setImmediate(r));
|
|
7621
7900
|
}
|
|
@@ -9730,6 +10009,304 @@ app23.delete("/", requireAdminSession, async (c) => {
|
|
|
9730
10009
|
});
|
|
9731
10010
|
var files_default = app23;
|
|
9732
10011
|
|
|
10012
|
+
// ../lib/graph-search/src/index.ts
|
|
10013
|
+
var import_dist = __toESM(require_dist());
|
|
10014
|
+
import { int } from "neo4j-driver";
|
|
10015
|
+
var VECTOR_WEIGHT = 0.7;
|
|
10016
|
+
var BM25_WEIGHT = 0.3;
|
|
10017
|
+
var FULLTEXT_INDEX_NAME = "knowledge_fulltext";
|
|
10018
|
+
function escapeLucene(query) {
|
|
10019
|
+
return query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, "\\$&");
|
|
10020
|
+
}
|
|
10021
|
+
function normaliseBm25Scores(scores) {
|
|
10022
|
+
if (scores.length === 0) return [];
|
|
10023
|
+
const min = Math.min(...scores);
|
|
10024
|
+
const max = Math.max(...scores);
|
|
10025
|
+
const range = max - min;
|
|
10026
|
+
if (range === 0) return scores.map(() => 1);
|
|
10027
|
+
return scores.map((s) => (s - min) / range);
|
|
10028
|
+
}
|
|
10029
|
+
var indexCache = null;
|
|
10030
|
+
async function discoverIndexes(session) {
|
|
10031
|
+
if (indexCache) return indexCache;
|
|
10032
|
+
const result = await session.run(
|
|
10033
|
+
`SHOW INDEXES YIELD name, labelsOrTypes, type WHERE type = 'VECTOR' RETURN name, labelsOrTypes`
|
|
10034
|
+
);
|
|
10035
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
10036
|
+
for (const record of result.records) {
|
|
10037
|
+
const name = record.get("name");
|
|
10038
|
+
const labels = record.get("labelsOrTypes");
|
|
10039
|
+
for (const label of labels) fresh.set(label, name);
|
|
10040
|
+
}
|
|
10041
|
+
indexCache = fresh;
|
|
10042
|
+
return fresh;
|
|
10043
|
+
}
|
|
10044
|
+
function buildKeywordFilter(keywords, keywordMatch = "any") {
|
|
10045
|
+
if (!keywords || keywords.length === 0) return void 0;
|
|
10046
|
+
const fn = keywordMatch === "all" ? "ALL" : "ANY";
|
|
10047
|
+
return {
|
|
10048
|
+
clause: `AND (node.keywords IS NULL OR ${fn}(kw IN $keywords WHERE kw IN node.keywords))`,
|
|
10049
|
+
params: { keywords: keywords.map((k) => k.toLowerCase().trim()) }
|
|
10050
|
+
};
|
|
10051
|
+
}
|
|
10052
|
+
async function bm25Only(session, params) {
|
|
10053
|
+
const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch } = params;
|
|
10054
|
+
const scopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10055
|
+
const agentClause = agentSlug ? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents" : "";
|
|
10056
|
+
const keywordFilter = buildKeywordFilter(keywords, keywordMatch);
|
|
10057
|
+
const kwClause = keywordFilter?.clause ?? "";
|
|
10058
|
+
const escaped = escapeLucene(query);
|
|
10059
|
+
try {
|
|
10060
|
+
const result = await session.run(
|
|
10061
|
+
`CALL db.index.fulltext.queryNodes($indexName, $query)
|
|
10062
|
+
YIELD node, score
|
|
10063
|
+
WHERE node.accountId = $accountId
|
|
10064
|
+
${scopeClause}
|
|
10065
|
+
${agentClause}
|
|
10066
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10067
|
+
${kwClause}
|
|
10068
|
+
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10069
|
+
ORDER BY score DESC
|
|
10070
|
+
LIMIT $limit`,
|
|
10071
|
+
{
|
|
10072
|
+
indexName: FULLTEXT_INDEX_NAME,
|
|
10073
|
+
query: escaped,
|
|
10074
|
+
accountId,
|
|
10075
|
+
limit: int(limit),
|
|
10076
|
+
...allowedScopes ? { allowedScopes } : {},
|
|
10077
|
+
...agentSlug ? { agentSlug } : {},
|
|
10078
|
+
...keywordFilter?.params ?? {}
|
|
10079
|
+
}
|
|
10080
|
+
);
|
|
10081
|
+
return result.records.map((r) => {
|
|
10082
|
+
const scoreRaw = r.get("score");
|
|
10083
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
|
|
10084
|
+
const node = r.get("node");
|
|
10085
|
+
return {
|
|
10086
|
+
nodeId: r.get("nodeId"),
|
|
10087
|
+
labels: r.get("nodeLabels"),
|
|
10088
|
+
properties: plainProperties(node.properties),
|
|
10089
|
+
score
|
|
10090
|
+
};
|
|
10091
|
+
});
|
|
10092
|
+
} catch (err) {
|
|
10093
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10094
|
+
if (msg.includes("index") || msg.includes("fulltext") || msg.includes("not found")) {
|
|
10095
|
+
return [];
|
|
10096
|
+
}
|
|
10097
|
+
throw err;
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
async function hybrid(session, embed2, params) {
|
|
10101
|
+
const {
|
|
10102
|
+
query,
|
|
10103
|
+
labels,
|
|
10104
|
+
accountId,
|
|
10105
|
+
limit,
|
|
10106
|
+
allowedScopes,
|
|
10107
|
+
keywords,
|
|
10108
|
+
keywordMatch = "any",
|
|
10109
|
+
agentSlug,
|
|
10110
|
+
keywordSubscriptions,
|
|
10111
|
+
expandHops = 1,
|
|
10112
|
+
degradeOnEmbedFailure = false
|
|
10113
|
+
} = params;
|
|
10114
|
+
let queryEmbedding;
|
|
10115
|
+
try {
|
|
10116
|
+
queryEmbedding = await embed2(query);
|
|
10117
|
+
} catch (err) {
|
|
10118
|
+
if (!degradeOnEmbedFailure) throw err;
|
|
10119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10120
|
+
const bm25Hits2 = await bm25Only(session, params);
|
|
10121
|
+
const results2 = bm25Hits2.map((h) => ({ ...h, related: [] }));
|
|
10122
|
+
return { mode: "bm25", results: results2, embedError: msg };
|
|
10123
|
+
}
|
|
10124
|
+
const labelToIndex = await discoverIndexes(session);
|
|
10125
|
+
const keywordFilter = buildKeywordFilter(keywords, keywordMatch);
|
|
10126
|
+
const keywordClause = keywordFilter?.clause ?? "";
|
|
10127
|
+
const keywordParams = keywordFilter?.params ?? {};
|
|
10128
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
10129
|
+
let indexesToQuery;
|
|
10130
|
+
if (labels && labels.length > 0) {
|
|
10131
|
+
indexesToQuery = labels.map((l) => labelToIndex.get(l)).filter((idx) => idx !== void 0);
|
|
10132
|
+
if (indexesToQuery.length === 0) {
|
|
10133
|
+
return { mode: "hybrid", results: [] };
|
|
10134
|
+
}
|
|
10135
|
+
} else {
|
|
10136
|
+
indexesToQuery = [...new Set(labelToIndex.values())];
|
|
10137
|
+
}
|
|
10138
|
+
const scopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10139
|
+
const scopeParams = allowedScopes ? { allowedScopes } : {};
|
|
10140
|
+
const agentClause = agentSlug ? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents" : "";
|
|
10141
|
+
const agentParams = agentSlug ? { agentSlug } : {};
|
|
10142
|
+
for (const indexName of indexesToQuery) {
|
|
10143
|
+
const vectorResult = await session.run(
|
|
10144
|
+
`CALL db.index.vector.queryNodes($indexName, $limit, $embedding)
|
|
10145
|
+
YIELD node, score
|
|
10146
|
+
WHERE node.accountId = $accountId
|
|
10147
|
+
${scopeClause}
|
|
10148
|
+
${agentClause}
|
|
10149
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10150
|
+
${keywordClause}
|
|
10151
|
+
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10152
|
+
ORDER BY score DESC
|
|
10153
|
+
LIMIT $limit`,
|
|
10154
|
+
{
|
|
10155
|
+
indexName,
|
|
10156
|
+
embedding: queryEmbedding,
|
|
10157
|
+
limit: int(limit),
|
|
10158
|
+
accountId,
|
|
10159
|
+
...scopeParams,
|
|
10160
|
+
...agentParams,
|
|
10161
|
+
...keywordParams
|
|
10162
|
+
}
|
|
10163
|
+
);
|
|
10164
|
+
for (const record of vectorResult.records) {
|
|
10165
|
+
const nodeId = record.get("nodeId");
|
|
10166
|
+
const scoreRaw = record.get("score");
|
|
10167
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
|
|
10168
|
+
const existing = scoreMap.get(nodeId);
|
|
10169
|
+
if (existing) {
|
|
10170
|
+
existing.vectorScore = Math.max(existing.vectorScore, score);
|
|
10171
|
+
} else {
|
|
10172
|
+
const node = record.get("node");
|
|
10173
|
+
scoreMap.set(nodeId, {
|
|
10174
|
+
nodeId,
|
|
10175
|
+
labels: record.get("nodeLabels"),
|
|
10176
|
+
properties: plainProperties(node.properties),
|
|
10177
|
+
vectorScore: score,
|
|
10178
|
+
bm25Score: 0
|
|
10179
|
+
});
|
|
10180
|
+
}
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
const bm25Hits = await bm25Only(session, params);
|
|
10184
|
+
if (bm25Hits.length > 0) {
|
|
10185
|
+
const rawScores = bm25Hits.map((h) => h.score);
|
|
10186
|
+
const normalised = normaliseBm25Scores(rawScores);
|
|
10187
|
+
for (let i = 0; i < bm25Hits.length; i++) {
|
|
10188
|
+
mergeBm25Hit(scoreMap, bm25Hits[i], normalised[i]);
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
if (keywordSubscriptions && keywordSubscriptions.length > 0) {
|
|
10192
|
+
for (const kw of keywordSubscriptions) {
|
|
10193
|
+
const kwHits = await bm25Only(session, {
|
|
10194
|
+
query: kw,
|
|
10195
|
+
accountId,
|
|
10196
|
+
limit,
|
|
10197
|
+
allowedScopes
|
|
10198
|
+
});
|
|
10199
|
+
if (kwHits.length === 0) continue;
|
|
10200
|
+
const rawScores = kwHits.map((h) => h.score);
|
|
10201
|
+
const normalised = normaliseBm25Scores(rawScores);
|
|
10202
|
+
for (let i = 0; i < kwHits.length; i++) {
|
|
10203
|
+
mergeBm25Hit(scoreMap, kwHits[i], normalised[i]);
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
const propScopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10207
|
+
const propResult = await session.run(
|
|
10208
|
+
`MATCH (node)
|
|
10209
|
+
WHERE node.accountId = $accountId
|
|
10210
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10211
|
+
AND node.keywords IS NOT NULL
|
|
10212
|
+
AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
|
|
10213
|
+
${propScopeClause}
|
|
10214
|
+
RETURN node, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10215
|
+
LIMIT $limit`,
|
|
10216
|
+
{
|
|
10217
|
+
accountId,
|
|
10218
|
+
kwSubs: keywordSubscriptions,
|
|
10219
|
+
limit: int(limit),
|
|
10220
|
+
...allowedScopes ? { allowedScopes } : {}
|
|
10221
|
+
}
|
|
10222
|
+
);
|
|
10223
|
+
for (const record of propResult.records) {
|
|
10224
|
+
const nodeId = record.get("nodeId");
|
|
10225
|
+
const existing = scoreMap.get(nodeId);
|
|
10226
|
+
if (existing) {
|
|
10227
|
+
existing.bm25Score = Math.max(existing.bm25Score, 1);
|
|
10228
|
+
} else {
|
|
10229
|
+
const node = record.get("node");
|
|
10230
|
+
scoreMap.set(nodeId, {
|
|
10231
|
+
nodeId,
|
|
10232
|
+
labels: record.get("nodeLabels"),
|
|
10233
|
+
properties: plainProperties(node.properties),
|
|
10234
|
+
vectorScore: 0,
|
|
10235
|
+
bm25Score: 1
|
|
10236
|
+
});
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
}
|
|
10240
|
+
const merged = [...scoreMap.values()].map((node) => ({
|
|
10241
|
+
...node,
|
|
10242
|
+
combinedScore: VECTOR_WEIGHT * node.vectorScore + BM25_WEIGHT * node.bm25Score
|
|
10243
|
+
})).sort((a, b) => b.combinedScore - a.combinedScore).slice(0, limit);
|
|
10244
|
+
const results = [];
|
|
10245
|
+
for (const node of merged) {
|
|
10246
|
+
const result = {
|
|
10247
|
+
nodeId: node.nodeId,
|
|
10248
|
+
labels: node.labels,
|
|
10249
|
+
properties: node.properties,
|
|
10250
|
+
score: node.combinedScore,
|
|
10251
|
+
related: []
|
|
10252
|
+
};
|
|
10253
|
+
if (expandHops > 0) {
|
|
10254
|
+
const expandScopeClause = allowedScopes ? "AND (related.scope IS NULL OR related.scope IN $allowedScopes)" : "";
|
|
10255
|
+
const expandAgentClause = agentSlug ? "AND (related.agents IS NULL OR $agentSlug IN related.agents)" : "";
|
|
10256
|
+
const expandResult = await session.run(
|
|
10257
|
+
`MATCH (n)-[r]-(related)
|
|
10258
|
+
WHERE elementId(n) = $nodeId
|
|
10259
|
+
AND ${(0, import_dist.notTrashed)("related")}
|
|
10260
|
+
${expandScopeClause}
|
|
10261
|
+
${expandAgentClause}
|
|
10262
|
+
RETURN type(r) AS relType,
|
|
10263
|
+
CASE WHEN startNode(r) = n THEN 'outgoing' ELSE 'incoming' END AS direction,
|
|
10264
|
+
labels(related) AS relatedLabels,
|
|
10265
|
+
related
|
|
10266
|
+
LIMIT 20`,
|
|
10267
|
+
{ nodeId: node.nodeId, ...scopeParams, ...agentParams }
|
|
10268
|
+
);
|
|
10269
|
+
for (const rec of expandResult.records) {
|
|
10270
|
+
const related = rec.get("related");
|
|
10271
|
+
result.related.push({
|
|
10272
|
+
relationship: rec.get("relType"),
|
|
10273
|
+
direction: rec.get("direction"),
|
|
10274
|
+
labels: rec.get("relatedLabels"),
|
|
10275
|
+
properties: plainProperties(related.properties)
|
|
10276
|
+
});
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
results.push(result);
|
|
10280
|
+
}
|
|
10281
|
+
return { mode: "hybrid", results };
|
|
10282
|
+
}
|
|
10283
|
+
function mergeBm25Hit(map, hit, normalisedScore) {
|
|
10284
|
+
const existing = map.get(hit.nodeId);
|
|
10285
|
+
if (existing) {
|
|
10286
|
+
existing.bm25Score = Math.max(existing.bm25Score, normalisedScore);
|
|
10287
|
+
} else {
|
|
10288
|
+
map.set(hit.nodeId, {
|
|
10289
|
+
nodeId: hit.nodeId,
|
|
10290
|
+
labels: hit.labels,
|
|
10291
|
+
properties: hit.properties,
|
|
10292
|
+
vectorScore: 0,
|
|
10293
|
+
bm25Score: normalisedScore
|
|
10294
|
+
});
|
|
10295
|
+
}
|
|
10296
|
+
}
|
|
10297
|
+
function plainProperties(properties) {
|
|
10298
|
+
const plain = {};
|
|
10299
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
10300
|
+
if (key === "embedding") continue;
|
|
10301
|
+
if (value && typeof value === "object" && "toNumber" in value) {
|
|
10302
|
+
plain[key] = value.toNumber();
|
|
10303
|
+
} else {
|
|
10304
|
+
plain[key] = value;
|
|
10305
|
+
}
|
|
10306
|
+
}
|
|
10307
|
+
return plain;
|
|
10308
|
+
}
|
|
10309
|
+
|
|
9733
10310
|
// server/routes/admin/graph-search.ts
|
|
9734
10311
|
var DEFAULT_LIMIT = 20;
|
|
9735
10312
|
var MAX_LIMIT = 100;
|
|
@@ -9740,23 +10317,40 @@ app24.get("/", requireAdminSession, async (c) => {
|
|
|
9740
10317
|
const rawLimit = c.req.query("limit");
|
|
9741
10318
|
const accountId = getAccountIdForSession(sessionKey);
|
|
9742
10319
|
if (!accountId) {
|
|
9743
|
-
console.error(`[
|
|
10320
|
+
console.error(`[graph-search] auth-rejected endpoint="/api/admin/graph-search" reason="no account for session"`);
|
|
9744
10321
|
return c.json({ error: "Account not found for session" }, 401);
|
|
9745
10322
|
}
|
|
9746
10323
|
if (!q) return c.json({ error: "q (search query) required" }, 400);
|
|
9747
10324
|
const parsedLimit = rawLimit ? parseInt(rawLimit, 10) : DEFAULT_LIMIT;
|
|
9748
10325
|
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, MAX_LIMIT) : DEFAULT_LIMIT;
|
|
9749
10326
|
const started = Date.now();
|
|
10327
|
+
const session = getSession();
|
|
9750
10328
|
try {
|
|
9751
|
-
const
|
|
10329
|
+
const res = await hybrid(session, embed, {
|
|
10330
|
+
query: q,
|
|
10331
|
+
accountId,
|
|
10332
|
+
limit,
|
|
10333
|
+
degradeOnEmbedFailure: true
|
|
10334
|
+
});
|
|
9752
10335
|
const elapsed = Date.now() - started;
|
|
9753
|
-
|
|
10336
|
+
if (res.embedError) {
|
|
10337
|
+
console.error(`[graph-search] embed-unavailable err="${res.embedError}" \u2014 bm25-only`);
|
|
10338
|
+
}
|
|
10339
|
+
console.error(`[graph-search] query="${q}" mode=${res.mode} results=${res.results.length} ms=${elapsed}`);
|
|
10340
|
+
const results = res.results.map((r) => ({
|
|
10341
|
+
nodeId: r.nodeId,
|
|
10342
|
+
labels: r.labels,
|
|
10343
|
+
properties: r.properties,
|
|
10344
|
+
score: r.score
|
|
10345
|
+
}));
|
|
9754
10346
|
return c.json({ results, elapsedMs: elapsed });
|
|
9755
10347
|
} catch (err) {
|
|
9756
10348
|
const elapsed = Date.now() - started;
|
|
9757
10349
|
const message = err instanceof Error ? err.message : String(err);
|
|
9758
|
-
console.error(`[
|
|
10350
|
+
console.error(`[graph-search] neo4j-unreachable query="${q}" ms=${elapsed} err="${message}"`);
|
|
9759
10351
|
return c.json({ error: `Graph search unavailable: ${message}` }, 503);
|
|
10352
|
+
} finally {
|
|
10353
|
+
await session.close();
|
|
9760
10354
|
}
|
|
9761
10355
|
});
|
|
9762
10356
|
var graph_search_default = app24;
|
|
@@ -10596,6 +11190,49 @@ app32.route("/file-attach", file_attach_default);
|
|
|
10596
11190
|
app32.route("/adherence", adherence_default);
|
|
10597
11191
|
var admin_default = app32;
|
|
10598
11192
|
|
|
11193
|
+
// app/lib/graph-health.ts
|
|
11194
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
11195
|
+
var timer = null;
|
|
11196
|
+
async function runGraphHealthTick() {
|
|
11197
|
+
const session = getSession();
|
|
11198
|
+
try {
|
|
11199
|
+
const totalResult = await session.run(
|
|
11200
|
+
`MATCH (n) WHERE NOT (n)--() RETURN count(n) AS total`
|
|
11201
|
+
);
|
|
11202
|
+
const total = totalResult.records[0]?.get("total")?.toNumber?.() ?? 0;
|
|
11203
|
+
const topResult = await session.run(
|
|
11204
|
+
`MATCH (n) WHERE NOT (n)--()
|
|
11205
|
+
WITH labels(n) AS lbls, count(*) AS c
|
|
11206
|
+
ORDER BY c DESC
|
|
11207
|
+
LIMIT 5
|
|
11208
|
+
RETURN collect({labels: lbls, count: c}) AS top`
|
|
11209
|
+
);
|
|
11210
|
+
const topFromDb = topResult.records[0]?.get("top") ?? [];
|
|
11211
|
+
const topStr = topFromDb.map((b) => {
|
|
11212
|
+
const labels = b.labels.join("+") || "(none)";
|
|
11213
|
+
const c = typeof b.count === "number" ? b.count : b.count.toNumber?.() ?? 0;
|
|
11214
|
+
return `${labels}:${c}`;
|
|
11215
|
+
}).join(",");
|
|
11216
|
+
console.error(`[graph-health] orphans total=${total} top=${topStr || "none"}`);
|
|
11217
|
+
} catch (err) {
|
|
11218
|
+
console.error(
|
|
11219
|
+
`[graph-health] query failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11220
|
+
);
|
|
11221
|
+
} finally {
|
|
11222
|
+
await session.close();
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
function startGraphHealthTimer() {
|
|
11226
|
+
if (timer) return;
|
|
11227
|
+
runGraphHealthTick().catch(() => {
|
|
11228
|
+
});
|
|
11229
|
+
timer = setInterval(() => {
|
|
11230
|
+
runGraphHealthTick().catch(() => {
|
|
11231
|
+
});
|
|
11232
|
+
}, HOUR_MS);
|
|
11233
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
11234
|
+
}
|
|
11235
|
+
|
|
10599
11236
|
// server/index.ts
|
|
10600
11237
|
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
10601
11238
|
var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join9(PLATFORM_ROOT6, "config", "brand.json") : "";
|
|
@@ -11282,6 +11919,7 @@ try {
|
|
|
11282
11919
|
console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
11283
11920
|
}
|
|
11284
11921
|
})();
|
|
11922
|
+
startGraphHealthTimer();
|
|
11285
11923
|
var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
|
|
11286
11924
|
var bootAccount = resolveAccount();
|
|
11287
11925
|
var bootAccountConfig = bootAccount?.config;
|