@rubytech/create-maxy 1.0.693 → 1.0.695
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/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/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/server.js +656 -21
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
|
});
|
|
@@ -5013,7 +5289,7 @@ async function processInbound(rawText, channel) {
|
|
|
5013
5289
|
return result;
|
|
5014
5290
|
}
|
|
5015
5291
|
const controller = new AbortController();
|
|
5016
|
-
const
|
|
5292
|
+
const timer2 = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
|
|
5017
5293
|
try {
|
|
5018
5294
|
const client = new Anthropic({ apiKey });
|
|
5019
5295
|
const response = await client.messages.create(
|
|
@@ -5089,7 +5365,7 @@ async function processInbound(rawText, channel) {
|
|
|
5089
5365
|
);
|
|
5090
5366
|
return defaultResult(rawText.trim(), latencyMs);
|
|
5091
5367
|
} finally {
|
|
5092
|
-
clearTimeout(
|
|
5368
|
+
clearTimeout(timer2);
|
|
5093
5369
|
}
|
|
5094
5370
|
}
|
|
5095
5371
|
|
|
@@ -7560,7 +7836,7 @@ function startScriptStreamTailer(opts) {
|
|
|
7560
7836
|
let buffer = "";
|
|
7561
7837
|
let stopped = false;
|
|
7562
7838
|
let pendingRead = false;
|
|
7563
|
-
let
|
|
7839
|
+
let timer2;
|
|
7564
7840
|
const processLine = (line) => {
|
|
7565
7841
|
const event = parseLine(line);
|
|
7566
7842
|
if (event) onEvent(event);
|
|
@@ -7607,15 +7883,15 @@ function startScriptStreamTailer(opts) {
|
|
|
7607
7883
|
if (stopped) return;
|
|
7608
7884
|
readDelta().catch(() => {
|
|
7609
7885
|
}).finally(() => {
|
|
7610
|
-
if (!stopped)
|
|
7886
|
+
if (!stopped) timer2 = setTimeout(tick, 200);
|
|
7611
7887
|
});
|
|
7612
7888
|
};
|
|
7613
|
-
|
|
7889
|
+
timer2 = setTimeout(tick, 0);
|
|
7614
7890
|
return {
|
|
7615
7891
|
async stop() {
|
|
7616
7892
|
if (stopped) return;
|
|
7617
7893
|
stopped = true;
|
|
7618
|
-
if (
|
|
7894
|
+
if (timer2) clearTimeout(timer2);
|
|
7619
7895
|
while (pendingRead) {
|
|
7620
7896
|
await new Promise((r) => setImmediate(r));
|
|
7621
7897
|
}
|
|
@@ -9730,6 +10006,304 @@ app23.delete("/", requireAdminSession, async (c) => {
|
|
|
9730
10006
|
});
|
|
9731
10007
|
var files_default = app23;
|
|
9732
10008
|
|
|
10009
|
+
// ../lib/graph-search/src/index.ts
|
|
10010
|
+
var import_dist = __toESM(require_dist());
|
|
10011
|
+
import { int } from "neo4j-driver";
|
|
10012
|
+
var VECTOR_WEIGHT = 0.7;
|
|
10013
|
+
var BM25_WEIGHT = 0.3;
|
|
10014
|
+
var FULLTEXT_INDEX_NAME = "knowledge_fulltext";
|
|
10015
|
+
function escapeLucene(query) {
|
|
10016
|
+
return query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, "\\$&");
|
|
10017
|
+
}
|
|
10018
|
+
function normaliseBm25Scores(scores) {
|
|
10019
|
+
if (scores.length === 0) return [];
|
|
10020
|
+
const min = Math.min(...scores);
|
|
10021
|
+
const max = Math.max(...scores);
|
|
10022
|
+
const range = max - min;
|
|
10023
|
+
if (range === 0) return scores.map(() => 1);
|
|
10024
|
+
return scores.map((s) => (s - min) / range);
|
|
10025
|
+
}
|
|
10026
|
+
var indexCache = null;
|
|
10027
|
+
async function discoverIndexes(session) {
|
|
10028
|
+
if (indexCache) return indexCache;
|
|
10029
|
+
const result = await session.run(
|
|
10030
|
+
`SHOW INDEXES YIELD name, labelsOrTypes, type WHERE type = 'VECTOR' RETURN name, labelsOrTypes`
|
|
10031
|
+
);
|
|
10032
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
10033
|
+
for (const record of result.records) {
|
|
10034
|
+
const name = record.get("name");
|
|
10035
|
+
const labels = record.get("labelsOrTypes");
|
|
10036
|
+
for (const label of labels) fresh.set(label, name);
|
|
10037
|
+
}
|
|
10038
|
+
indexCache = fresh;
|
|
10039
|
+
return fresh;
|
|
10040
|
+
}
|
|
10041
|
+
function buildKeywordFilter(keywords, keywordMatch = "any") {
|
|
10042
|
+
if (!keywords || keywords.length === 0) return void 0;
|
|
10043
|
+
const fn = keywordMatch === "all" ? "ALL" : "ANY";
|
|
10044
|
+
return {
|
|
10045
|
+
clause: `AND (node.keywords IS NULL OR ${fn}(kw IN $keywords WHERE kw IN node.keywords))`,
|
|
10046
|
+
params: { keywords: keywords.map((k) => k.toLowerCase().trim()) }
|
|
10047
|
+
};
|
|
10048
|
+
}
|
|
10049
|
+
async function bm25Only(session, params) {
|
|
10050
|
+
const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch } = params;
|
|
10051
|
+
const scopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10052
|
+
const agentClause = agentSlug ? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents" : "";
|
|
10053
|
+
const keywordFilter = buildKeywordFilter(keywords, keywordMatch);
|
|
10054
|
+
const kwClause = keywordFilter?.clause ?? "";
|
|
10055
|
+
const escaped = escapeLucene(query);
|
|
10056
|
+
try {
|
|
10057
|
+
const result = await session.run(
|
|
10058
|
+
`CALL db.index.fulltext.queryNodes($indexName, $query)
|
|
10059
|
+
YIELD node, score
|
|
10060
|
+
WHERE node.accountId = $accountId
|
|
10061
|
+
${scopeClause}
|
|
10062
|
+
${agentClause}
|
|
10063
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10064
|
+
${kwClause}
|
|
10065
|
+
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10066
|
+
ORDER BY score DESC
|
|
10067
|
+
LIMIT $limit`,
|
|
10068
|
+
{
|
|
10069
|
+
indexName: FULLTEXT_INDEX_NAME,
|
|
10070
|
+
query: escaped,
|
|
10071
|
+
accountId,
|
|
10072
|
+
limit: int(limit),
|
|
10073
|
+
...allowedScopes ? { allowedScopes } : {},
|
|
10074
|
+
...agentSlug ? { agentSlug } : {},
|
|
10075
|
+
...keywordFilter?.params ?? {}
|
|
10076
|
+
}
|
|
10077
|
+
);
|
|
10078
|
+
return result.records.map((r) => {
|
|
10079
|
+
const scoreRaw = r.get("score");
|
|
10080
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
|
|
10081
|
+
const node = r.get("node");
|
|
10082
|
+
return {
|
|
10083
|
+
nodeId: r.get("nodeId"),
|
|
10084
|
+
labels: r.get("nodeLabels"),
|
|
10085
|
+
properties: plainProperties(node.properties),
|
|
10086
|
+
score
|
|
10087
|
+
};
|
|
10088
|
+
});
|
|
10089
|
+
} catch (err) {
|
|
10090
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10091
|
+
if (msg.includes("index") || msg.includes("fulltext") || msg.includes("not found")) {
|
|
10092
|
+
return [];
|
|
10093
|
+
}
|
|
10094
|
+
throw err;
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
async function hybrid(session, embed2, params) {
|
|
10098
|
+
const {
|
|
10099
|
+
query,
|
|
10100
|
+
labels,
|
|
10101
|
+
accountId,
|
|
10102
|
+
limit,
|
|
10103
|
+
allowedScopes,
|
|
10104
|
+
keywords,
|
|
10105
|
+
keywordMatch = "any",
|
|
10106
|
+
agentSlug,
|
|
10107
|
+
keywordSubscriptions,
|
|
10108
|
+
expandHops = 1,
|
|
10109
|
+
degradeOnEmbedFailure = false
|
|
10110
|
+
} = params;
|
|
10111
|
+
let queryEmbedding;
|
|
10112
|
+
try {
|
|
10113
|
+
queryEmbedding = await embed2(query);
|
|
10114
|
+
} catch (err) {
|
|
10115
|
+
if (!degradeOnEmbedFailure) throw err;
|
|
10116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10117
|
+
const bm25Hits2 = await bm25Only(session, params);
|
|
10118
|
+
const results2 = bm25Hits2.map((h) => ({ ...h, related: [] }));
|
|
10119
|
+
return { mode: "bm25", results: results2, embedError: msg };
|
|
10120
|
+
}
|
|
10121
|
+
const labelToIndex = await discoverIndexes(session);
|
|
10122
|
+
const keywordFilter = buildKeywordFilter(keywords, keywordMatch);
|
|
10123
|
+
const keywordClause = keywordFilter?.clause ?? "";
|
|
10124
|
+
const keywordParams = keywordFilter?.params ?? {};
|
|
10125
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
10126
|
+
let indexesToQuery;
|
|
10127
|
+
if (labels && labels.length > 0) {
|
|
10128
|
+
indexesToQuery = labels.map((l) => labelToIndex.get(l)).filter((idx) => idx !== void 0);
|
|
10129
|
+
if (indexesToQuery.length === 0) {
|
|
10130
|
+
return { mode: "hybrid", results: [] };
|
|
10131
|
+
}
|
|
10132
|
+
} else {
|
|
10133
|
+
indexesToQuery = [...new Set(labelToIndex.values())];
|
|
10134
|
+
}
|
|
10135
|
+
const scopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10136
|
+
const scopeParams = allowedScopes ? { allowedScopes } : {};
|
|
10137
|
+
const agentClause = agentSlug ? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents" : "";
|
|
10138
|
+
const agentParams = agentSlug ? { agentSlug } : {};
|
|
10139
|
+
for (const indexName of indexesToQuery) {
|
|
10140
|
+
const vectorResult = await session.run(
|
|
10141
|
+
`CALL db.index.vector.queryNodes($indexName, $limit, $embedding)
|
|
10142
|
+
YIELD node, score
|
|
10143
|
+
WHERE node.accountId = $accountId
|
|
10144
|
+
${scopeClause}
|
|
10145
|
+
${agentClause}
|
|
10146
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10147
|
+
${keywordClause}
|
|
10148
|
+
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10149
|
+
ORDER BY score DESC
|
|
10150
|
+
LIMIT $limit`,
|
|
10151
|
+
{
|
|
10152
|
+
indexName,
|
|
10153
|
+
embedding: queryEmbedding,
|
|
10154
|
+
limit: int(limit),
|
|
10155
|
+
accountId,
|
|
10156
|
+
...scopeParams,
|
|
10157
|
+
...agentParams,
|
|
10158
|
+
...keywordParams
|
|
10159
|
+
}
|
|
10160
|
+
);
|
|
10161
|
+
for (const record of vectorResult.records) {
|
|
10162
|
+
const nodeId = record.get("nodeId");
|
|
10163
|
+
const scoreRaw = record.get("score");
|
|
10164
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
|
|
10165
|
+
const existing = scoreMap.get(nodeId);
|
|
10166
|
+
if (existing) {
|
|
10167
|
+
existing.vectorScore = Math.max(existing.vectorScore, score);
|
|
10168
|
+
} else {
|
|
10169
|
+
const node = record.get("node");
|
|
10170
|
+
scoreMap.set(nodeId, {
|
|
10171
|
+
nodeId,
|
|
10172
|
+
labels: record.get("nodeLabels"),
|
|
10173
|
+
properties: plainProperties(node.properties),
|
|
10174
|
+
vectorScore: score,
|
|
10175
|
+
bm25Score: 0
|
|
10176
|
+
});
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
}
|
|
10180
|
+
const bm25Hits = await bm25Only(session, params);
|
|
10181
|
+
if (bm25Hits.length > 0) {
|
|
10182
|
+
const rawScores = bm25Hits.map((h) => h.score);
|
|
10183
|
+
const normalised = normaliseBm25Scores(rawScores);
|
|
10184
|
+
for (let i = 0; i < bm25Hits.length; i++) {
|
|
10185
|
+
mergeBm25Hit(scoreMap, bm25Hits[i], normalised[i]);
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
if (keywordSubscriptions && keywordSubscriptions.length > 0) {
|
|
10189
|
+
for (const kw of keywordSubscriptions) {
|
|
10190
|
+
const kwHits = await bm25Only(session, {
|
|
10191
|
+
query: kw,
|
|
10192
|
+
accountId,
|
|
10193
|
+
limit,
|
|
10194
|
+
allowedScopes
|
|
10195
|
+
});
|
|
10196
|
+
if (kwHits.length === 0) continue;
|
|
10197
|
+
const rawScores = kwHits.map((h) => h.score);
|
|
10198
|
+
const normalised = normaliseBm25Scores(rawScores);
|
|
10199
|
+
for (let i = 0; i < kwHits.length; i++) {
|
|
10200
|
+
mergeBm25Hit(scoreMap, kwHits[i], normalised[i]);
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
const propScopeClause = allowedScopes ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)" : "";
|
|
10204
|
+
const propResult = await session.run(
|
|
10205
|
+
`MATCH (node)
|
|
10206
|
+
WHERE node.accountId = $accountId
|
|
10207
|
+
AND ${(0, import_dist.notTrashed)("node")}
|
|
10208
|
+
AND node.keywords IS NOT NULL
|
|
10209
|
+
AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
|
|
10210
|
+
${propScopeClause}
|
|
10211
|
+
RETURN node, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
10212
|
+
LIMIT $limit`,
|
|
10213
|
+
{
|
|
10214
|
+
accountId,
|
|
10215
|
+
kwSubs: keywordSubscriptions,
|
|
10216
|
+
limit: int(limit),
|
|
10217
|
+
...allowedScopes ? { allowedScopes } : {}
|
|
10218
|
+
}
|
|
10219
|
+
);
|
|
10220
|
+
for (const record of propResult.records) {
|
|
10221
|
+
const nodeId = record.get("nodeId");
|
|
10222
|
+
const existing = scoreMap.get(nodeId);
|
|
10223
|
+
if (existing) {
|
|
10224
|
+
existing.bm25Score = Math.max(existing.bm25Score, 1);
|
|
10225
|
+
} else {
|
|
10226
|
+
const node = record.get("node");
|
|
10227
|
+
scoreMap.set(nodeId, {
|
|
10228
|
+
nodeId,
|
|
10229
|
+
labels: record.get("nodeLabels"),
|
|
10230
|
+
properties: plainProperties(node.properties),
|
|
10231
|
+
vectorScore: 0,
|
|
10232
|
+
bm25Score: 1
|
|
10233
|
+
});
|
|
10234
|
+
}
|
|
10235
|
+
}
|
|
10236
|
+
}
|
|
10237
|
+
const merged = [...scoreMap.values()].map((node) => ({
|
|
10238
|
+
...node,
|
|
10239
|
+
combinedScore: VECTOR_WEIGHT * node.vectorScore + BM25_WEIGHT * node.bm25Score
|
|
10240
|
+
})).sort((a, b) => b.combinedScore - a.combinedScore).slice(0, limit);
|
|
10241
|
+
const results = [];
|
|
10242
|
+
for (const node of merged) {
|
|
10243
|
+
const result = {
|
|
10244
|
+
nodeId: node.nodeId,
|
|
10245
|
+
labels: node.labels,
|
|
10246
|
+
properties: node.properties,
|
|
10247
|
+
score: node.combinedScore,
|
|
10248
|
+
related: []
|
|
10249
|
+
};
|
|
10250
|
+
if (expandHops > 0) {
|
|
10251
|
+
const expandScopeClause = allowedScopes ? "AND (related.scope IS NULL OR related.scope IN $allowedScopes)" : "";
|
|
10252
|
+
const expandAgentClause = agentSlug ? "AND (related.agents IS NULL OR $agentSlug IN related.agents)" : "";
|
|
10253
|
+
const expandResult = await session.run(
|
|
10254
|
+
`MATCH (n)-[r]-(related)
|
|
10255
|
+
WHERE elementId(n) = $nodeId
|
|
10256
|
+
AND ${(0, import_dist.notTrashed)("related")}
|
|
10257
|
+
${expandScopeClause}
|
|
10258
|
+
${expandAgentClause}
|
|
10259
|
+
RETURN type(r) AS relType,
|
|
10260
|
+
CASE WHEN startNode(r) = n THEN 'outgoing' ELSE 'incoming' END AS direction,
|
|
10261
|
+
labels(related) AS relatedLabels,
|
|
10262
|
+
related
|
|
10263
|
+
LIMIT 20`,
|
|
10264
|
+
{ nodeId: node.nodeId, ...scopeParams, ...agentParams }
|
|
10265
|
+
);
|
|
10266
|
+
for (const rec of expandResult.records) {
|
|
10267
|
+
const related = rec.get("related");
|
|
10268
|
+
result.related.push({
|
|
10269
|
+
relationship: rec.get("relType"),
|
|
10270
|
+
direction: rec.get("direction"),
|
|
10271
|
+
labels: rec.get("relatedLabels"),
|
|
10272
|
+
properties: plainProperties(related.properties)
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10275
|
+
}
|
|
10276
|
+
results.push(result);
|
|
10277
|
+
}
|
|
10278
|
+
return { mode: "hybrid", results };
|
|
10279
|
+
}
|
|
10280
|
+
function mergeBm25Hit(map, hit, normalisedScore) {
|
|
10281
|
+
const existing = map.get(hit.nodeId);
|
|
10282
|
+
if (existing) {
|
|
10283
|
+
existing.bm25Score = Math.max(existing.bm25Score, normalisedScore);
|
|
10284
|
+
} else {
|
|
10285
|
+
map.set(hit.nodeId, {
|
|
10286
|
+
nodeId: hit.nodeId,
|
|
10287
|
+
labels: hit.labels,
|
|
10288
|
+
properties: hit.properties,
|
|
10289
|
+
vectorScore: 0,
|
|
10290
|
+
bm25Score: normalisedScore
|
|
10291
|
+
});
|
|
10292
|
+
}
|
|
10293
|
+
}
|
|
10294
|
+
function plainProperties(properties) {
|
|
10295
|
+
const plain = {};
|
|
10296
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
10297
|
+
if (key === "embedding") continue;
|
|
10298
|
+
if (value && typeof value === "object" && "toNumber" in value) {
|
|
10299
|
+
plain[key] = value.toNumber();
|
|
10300
|
+
} else {
|
|
10301
|
+
plain[key] = value;
|
|
10302
|
+
}
|
|
10303
|
+
}
|
|
10304
|
+
return plain;
|
|
10305
|
+
}
|
|
10306
|
+
|
|
9733
10307
|
// server/routes/admin/graph-search.ts
|
|
9734
10308
|
var DEFAULT_LIMIT = 20;
|
|
9735
10309
|
var MAX_LIMIT = 100;
|
|
@@ -9740,23 +10314,40 @@ app24.get("/", requireAdminSession, async (c) => {
|
|
|
9740
10314
|
const rawLimit = c.req.query("limit");
|
|
9741
10315
|
const accountId = getAccountIdForSession(sessionKey);
|
|
9742
10316
|
if (!accountId) {
|
|
9743
|
-
console.error(`[
|
|
10317
|
+
console.error(`[graph-search] auth-rejected endpoint="/api/admin/graph-search" reason="no account for session"`);
|
|
9744
10318
|
return c.json({ error: "Account not found for session" }, 401);
|
|
9745
10319
|
}
|
|
9746
10320
|
if (!q) return c.json({ error: "q (search query) required" }, 400);
|
|
9747
10321
|
const parsedLimit = rawLimit ? parseInt(rawLimit, 10) : DEFAULT_LIMIT;
|
|
9748
10322
|
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, MAX_LIMIT) : DEFAULT_LIMIT;
|
|
9749
10323
|
const started = Date.now();
|
|
10324
|
+
const session = getSession();
|
|
9750
10325
|
try {
|
|
9751
|
-
const
|
|
10326
|
+
const res = await hybrid(session, embed, {
|
|
10327
|
+
query: q,
|
|
10328
|
+
accountId,
|
|
10329
|
+
limit,
|
|
10330
|
+
degradeOnEmbedFailure: true
|
|
10331
|
+
});
|
|
9752
10332
|
const elapsed = Date.now() - started;
|
|
9753
|
-
|
|
10333
|
+
if (res.embedError) {
|
|
10334
|
+
console.error(`[graph-search] embed-unavailable err="${res.embedError}" \u2014 bm25-only`);
|
|
10335
|
+
}
|
|
10336
|
+
console.error(`[graph-search] query="${q}" mode=${res.mode} results=${res.results.length} ms=${elapsed}`);
|
|
10337
|
+
const results = res.results.map((r) => ({
|
|
10338
|
+
nodeId: r.nodeId,
|
|
10339
|
+
labels: r.labels,
|
|
10340
|
+
properties: r.properties,
|
|
10341
|
+
score: r.score
|
|
10342
|
+
}));
|
|
9754
10343
|
return c.json({ results, elapsedMs: elapsed });
|
|
9755
10344
|
} catch (err) {
|
|
9756
10345
|
const elapsed = Date.now() - started;
|
|
9757
10346
|
const message = err instanceof Error ? err.message : String(err);
|
|
9758
|
-
console.error(`[
|
|
10347
|
+
console.error(`[graph-search] neo4j-unreachable query="${q}" ms=${elapsed} err="${message}"`);
|
|
9759
10348
|
return c.json({ error: `Graph search unavailable: ${message}` }, 503);
|
|
10349
|
+
} finally {
|
|
10350
|
+
await session.close();
|
|
9760
10351
|
}
|
|
9761
10352
|
});
|
|
9762
10353
|
var graph_search_default = app24;
|
|
@@ -10596,6 +11187,49 @@ app32.route("/file-attach", file_attach_default);
|
|
|
10596
11187
|
app32.route("/adherence", adherence_default);
|
|
10597
11188
|
var admin_default = app32;
|
|
10598
11189
|
|
|
11190
|
+
// app/lib/graph-health.ts
|
|
11191
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
11192
|
+
var timer = null;
|
|
11193
|
+
async function runGraphHealthTick() {
|
|
11194
|
+
const session = getSession();
|
|
11195
|
+
try {
|
|
11196
|
+
const totalResult = await session.run(
|
|
11197
|
+
`MATCH (n) WHERE NOT (n)--() RETURN count(n) AS total`
|
|
11198
|
+
);
|
|
11199
|
+
const total = totalResult.records[0]?.get("total")?.toNumber?.() ?? 0;
|
|
11200
|
+
const topResult = await session.run(
|
|
11201
|
+
`MATCH (n) WHERE NOT (n)--()
|
|
11202
|
+
WITH labels(n) AS lbls, count(*) AS c
|
|
11203
|
+
ORDER BY c DESC
|
|
11204
|
+
LIMIT 5
|
|
11205
|
+
RETURN collect({labels: lbls, count: c}) AS top`
|
|
11206
|
+
);
|
|
11207
|
+
const topFromDb = topResult.records[0]?.get("top") ?? [];
|
|
11208
|
+
const topStr = topFromDb.map((b) => {
|
|
11209
|
+
const labels = b.labels.join("+") || "(none)";
|
|
11210
|
+
const c = typeof b.count === "number" ? b.count : b.count.toNumber?.() ?? 0;
|
|
11211
|
+
return `${labels}:${c}`;
|
|
11212
|
+
}).join(",");
|
|
11213
|
+
console.error(`[graph-health] orphans total=${total} top=${topStr || "none"}`);
|
|
11214
|
+
} catch (err) {
|
|
11215
|
+
console.error(
|
|
11216
|
+
`[graph-health] query failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11217
|
+
);
|
|
11218
|
+
} finally {
|
|
11219
|
+
await session.close();
|
|
11220
|
+
}
|
|
11221
|
+
}
|
|
11222
|
+
function startGraphHealthTimer() {
|
|
11223
|
+
if (timer) return;
|
|
11224
|
+
runGraphHealthTick().catch(() => {
|
|
11225
|
+
});
|
|
11226
|
+
timer = setInterval(() => {
|
|
11227
|
+
runGraphHealthTick().catch(() => {
|
|
11228
|
+
});
|
|
11229
|
+
}, HOUR_MS);
|
|
11230
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
11231
|
+
}
|
|
11232
|
+
|
|
10599
11233
|
// server/index.ts
|
|
10600
11234
|
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
10601
11235
|
var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join9(PLATFORM_ROOT6, "config", "brand.json") : "";
|
|
@@ -11282,6 +11916,7 @@ try {
|
|
|
11282
11916
|
console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
11283
11917
|
}
|
|
11284
11918
|
})();
|
|
11919
|
+
startGraphHealthTimer();
|
|
11285
11920
|
var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
|
|
11286
11921
|
var bootAccount = resolveAccount();
|
|
11287
11922
|
var bootAccountConfig = bootAccount?.config;
|