@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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts +127 -0
  3. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/graph-search/dist/index.js +393 -0
  5. package/payload/platform/lib/graph-search/dist/index.js.map +1 -0
  6. package/payload/platform/lib/graph-search/src/__tests__/bm25-only.test.ts +129 -0
  7. package/payload/platform/lib/graph-search/src/__tests__/escape-and-normalise.test.ts +53 -0
  8. package/payload/platform/lib/graph-search/src/__tests__/hybrid.test.ts +190 -0
  9. package/payload/platform/lib/graph-search/src/index.ts +498 -0
  10. package/payload/platform/lib/graph-search/tsconfig.json +9 -0
  11. package/payload/platform/lib/graph-search/vitest.config.ts +9 -0
  12. package/payload/platform/lib/graph-write/dist/index.d.ts +61 -0
  13. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -0
  14. package/payload/platform/lib/graph-write/dist/index.js +97 -0
  15. package/payload/platform/lib/graph-write/dist/index.js.map +1 -0
  16. package/payload/platform/lib/graph-write/src/index.ts +167 -0
  17. package/payload/platform/lib/graph-write/tsconfig.json +8 -0
  18. package/payload/platform/package.json +2 -2
  19. package/payload/platform/plugins/admin/mcp/dist/index.js +19 -8
  20. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  21. package/payload/platform/plugins/contacts/mcp/dist/index.js +27 -3
  22. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +4 -0
  24. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
  25. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +10 -6
  26. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  27. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +2 -0
  28. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
  29. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +43 -36
  30. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
  31. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  32. package/payload/platform/plugins/memory/mcp/dist/index.js +44 -3
  33. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +3 -32
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +18 -381
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +9 -5
  39. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  40. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -23
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  42. package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
  43. package/payload/platform/plugins/scheduling/mcp/dist/index.js +8 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +2 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +24 -10
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  49. package/payload/platform/plugins/tasks/mcp/dist/index.js +8 -2
  50. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  51. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +2 -0
  52. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
  53. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -18
  54. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
  55. package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +12 -2
  56. package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
  57. package/payload/server/chunk-IAIGB5WN.js +11406 -0
  58. package/payload/server/maxy-edge.js +1 -1
  59. package/payload/server/server.js +656 -21
@@ -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-Q6NDXCM6.js";
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 timer = new Promise(
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
- timer
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 timer = setTimeout(() => {
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(timer);
2937
+ clearTimeout(timer2);
2662
2938
  resolve22(value);
2663
2939
  },
2664
2940
  (err) => {
2665
- clearTimeout(timer);
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 timer = setTimeout(() => {
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 timer = setTimeout(resolve22, delay);
4153
+ const timer2 = setTimeout(resolve22, delay);
3878
4154
  conn.abortController.signal.addEventListener("abort", () => {
3879
- clearTimeout(timer);
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 timer = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
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(timer);
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 timer;
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) timer = setTimeout(tick, 200);
7886
+ if (!stopped) timer2 = setTimeout(tick, 200);
7611
7887
  });
7612
7888
  };
7613
- timer = setTimeout(tick, 0);
7889
+ timer2 = setTimeout(tick, 0);
7614
7890
  return {
7615
7891
  async stop() {
7616
7892
  if (stopped) return;
7617
7893
  stopped = true;
7618
- if (timer) clearTimeout(timer);
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(`[data] auth-rejected endpoint="/api/admin/graph-search" reason="no account for session"`);
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 results = await searchKnowledgeFulltext(accountId, q, limit);
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
- console.error(`[data] graph-search query="${q}" results=${results.length} ms=${elapsed}`);
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(`[data] graph-search neo4j-unreachable query="${q}" ms=${elapsed} err="${message}"`);
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;