@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.
Files changed (64) 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/admin/skills/unzip-attachment/SKILL.md +58 -0
  22. package/payload/platform/plugins/admin/skills/unzip-attachment/references/safety.md +81 -0
  23. package/payload/platform/plugins/contacts/mcp/dist/index.js +27 -3
  24. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  25. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +4 -0
  26. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
  27. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +10 -6
  28. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  29. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +2 -0
  30. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
  31. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +43 -36
  32. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
  33. package/payload/platform/plugins/docs/references/attachments.md +44 -0
  34. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  35. package/payload/platform/plugins/memory/mcp/dist/index.js +44 -3
  36. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +3 -32
  38. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +18 -381
  40. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +9 -5
  42. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -23
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  45. package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/index.js +8 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +2 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +24 -10
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  52. package/payload/platform/plugins/tasks/mcp/dist/index.js +8 -2
  53. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  54. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +2 -0
  55. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
  56. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -18
  57. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
  58. package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +12 -2
  59. package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
  60. package/payload/server/chunk-IAIGB5WN.js +11406 -0
  61. package/payload/server/maxy-edge.js +1 -1
  62. package/payload/server/public/assets/{admin-zbb1g-mh.js → admin-BZSstsyc.js} +60 -60
  63. package/payload/server/public/index.html +1 -1
  64. package/payload/server/server.js +660 -22
@@ -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
  });
@@ -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 timer = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
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(timer);
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 timer;
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) timer = setTimeout(tick, 200);
7889
+ if (!stopped) timer2 = setTimeout(tick, 200);
7611
7890
  });
7612
7891
  };
7613
- timer = setTimeout(tick, 0);
7892
+ timer2 = setTimeout(tick, 0);
7614
7893
  return {
7615
7894
  async stop() {
7616
7895
  if (stopped) return;
7617
7896
  stopped = true;
7618
- if (timer) clearTimeout(timer);
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(`[data] auth-rejected endpoint="/api/admin/graph-search" reason="no account for session"`);
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 results = await searchKnowledgeFulltext(accountId, q, limit);
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
- console.error(`[data] graph-search query="${q}" results=${results.length} ms=${elapsed}`);
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(`[data] graph-search neo4j-unreachable query="${q}" ms=${elapsed} err="${message}"`);
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;