@rubytech/create-maxy-code 0.1.140 → 0.1.143

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 (139) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/lib/graph-style/dist/index.d.ts +6 -4
  4. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-style/dist/index.js +10 -23
  6. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  7. package/payload/platform/lib/graph-style/src/index.ts +10 -20
  8. package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
  9. package/payload/platform/lib/graph-trash/dist/index.js +6 -1
  10. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  11. package/payload/platform/lib/graph-trash/src/index.ts +6 -1
  12. package/payload/platform/neo4j/edge-annotations.json +3 -3
  13. package/payload/platform/neo4j/schema.cypher +81 -119
  14. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  15. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  16. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts.map +1 -1
  17. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +14 -8
  18. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js.map +1 -1
  19. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
  20. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts.map +1 -1
  21. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +10 -7
  22. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js.map +1 -1
  23. package/payload/platform/plugins/docs/references/internals.md +1 -1
  24. package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
  25. package/payload/platform/plugins/docs/references/neo4j.md +3 -3
  26. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
  27. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  28. package/payload/platform/plugins/email/PLUGIN.md +3 -7
  29. package/payload/platform/plugins/email/mcp/dist/index.js +7 -46
  30. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  31. package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
  32. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +4 -15
  33. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
  34. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +51 -43
  35. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  36. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +223 -229
  37. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  38. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +7 -4
  39. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
  40. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +14 -4
  41. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
  42. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +69 -84
  43. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
  44. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  45. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +49 -64
  46. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  47. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
  48. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +1 -2
  49. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
  50. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  51. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  52. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +12 -13
  53. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
  54. package/payload/platform/plugins/memory/mcp/dist/index.js +17 -15
  55. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  56. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
  58. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
  60. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
  62. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +13 -7
  63. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
  64. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts +3 -0
  65. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts.map +1 -0
  66. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +101 -0
  67. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -0
  68. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -1
  69. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +2 -1
  70. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -1
  71. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +5 -3
  72. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +12 -10
  74. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
  76. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +7 -4
  77. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +3 -3
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js +1 -1
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js.map +1 -1
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +47 -39
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  84. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +7 -6
  85. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
  86. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +12 -11
  87. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +59 -1
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +326 -4
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +31 -27
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +72 -49
  95. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  96. package/payload/platform/plugins/memory/references/schema-base.md +5 -6
  97. package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +1 -1
  98. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +16 -15
  99. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +8 -8
  100. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +14 -13
  101. package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +13 -9
  102. package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +11 -9
  103. package/payload/platform/plugins/notion-import/skills/notion-import/references/page-tree.md +12 -12
  104. package/payload/platform/plugins/scheduling/PLUGIN.md +18 -0
  105. package/payload/platform/plugins/scheduling/mcp/dist/index.js +93 -0
  106. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  107. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts +80 -0
  108. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts.map +1 -0
  109. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js +395 -0
  110. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js.map +1 -0
  111. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts +16 -0
  112. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts.map +1 -1
  113. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js +111 -2
  114. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js.map +1 -1
  115. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts +20 -0
  116. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts.map +1 -0
  117. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js +63 -0
  118. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js.map +1 -0
  119. package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
  120. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  121. package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +8 -0
  122. package/payload/platform/plugins/x-import/PLUGIN.md +33 -0
  123. package/payload/platform/plugins/x-import/references/archive-shape.md +124 -0
  124. package/payload/platform/plugins/x-import/references/transcript-shape.md +121 -0
  125. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +123 -0
  126. package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +1 -1
  127. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  128. package/payload/platform/templates/specialists/agents/librarian.md +4 -4
  129. package/payload/server/public/assets/{admin-BM9aXUUh.js → admin-_pm0RRTE.js} +1 -1
  130. package/payload/server/public/assets/{data-Ds2ECtfP.js → data-b6q64Ot4.js} +1 -1
  131. package/payload/server/public/assets/{graph-BzxObKMM.js → graph-D1SVlBLb.js} +1 -1
  132. package/payload/server/public/assets/graph-labels-BiDGzVs2.js +1 -0
  133. package/payload/server/public/assets/{page-BlkBsjP2.js → page-BV6E4J2q.js} +1 -1
  134. package/payload/server/public/assets/{page-h_SA5SVQ.js → page-DwFigpyR.js} +1 -1
  135. package/payload/server/public/data.html +3 -3
  136. package/payload/server/public/graph.html +3 -3
  137. package/payload/server/public/index.html +4 -4
  138. package/payload/server/server.js +18 -21
  139. package/payload/server/public/assets/graph-labels-Dwgep_nr.js +0 -1
@@ -15,131 +15,212 @@ export function stripAngleBrackets(value) {
15
15
  }
16
16
  return trimmed;
17
17
  }
18
+ /** Globally-unique attachmentId for a thread, scoped by account + root messageId. */
19
+ function deriveThreadAttachmentId(accountId, rootMessageId) {
20
+ return createHash("sha256")
21
+ .update(`email:${accountId}:${rootMessageId}`)
22
+ .digest("hex");
23
+ }
24
+ /** Fallback attachmentId for a message with no Message-ID header. */
25
+ function deriveFallbackAttachmentId(data) {
26
+ return createHash("sha256")
27
+ .update(`email-fallback:${data.accountId}:${data.uidValidity}:${data.uid}:${data.mailbox}`)
28
+ .digest("hex");
29
+ }
30
+ function normaliseAddress(addr) {
31
+ return addr.trim().toLowerCase();
32
+ }
33
+ function renderMessageBlock(data, messageId) {
34
+ const headers = [
35
+ `Message-ID: ${messageId}`,
36
+ `From: ${data.fromName ? `${data.fromName} <${data.fromAddress}>` : data.fromAddress}`,
37
+ `To: ${data.toAddresses.join(", ")}`,
38
+ data.ccAddresses.length > 0 ? `Cc: ${data.ccAddresses.join(", ")}` : null,
39
+ `Date: ${data.receivedAt}`,
40
+ `Subject: ${data.subject}`,
41
+ ]
42
+ .filter((line) => line !== null)
43
+ .join("\n");
44
+ return `--- BEGIN MESSAGE ---\n${headers}\n\n${data.bodyPreview}\n--- END MESSAGE ---`;
45
+ }
18
46
  /**
19
- * Derive a deterministic emailId for deduplication.
20
- * Uses RFC 2822 Message-ID when available (stable across UIDVALIDITY resets).
21
- * Falls back to SHA-256 of UID + UIDVALIDITY + mailbox + recipientAddress.
47
+ * Find the thread KD that already contains the given message-id in its
48
+ * messageIds array, scoped to one account. Returns null when no match
49
+ * (the message starts a new thread or is an out-of-order reply whose
50
+ * parent thread has not yet been ingested).
22
51
  */
23
- export function deriveEmailId(data) {
24
- if (data.messageId) {
25
- return createHash("sha256")
26
- .update(`msgid:${data.messageId}:${data.recipientAddress}`)
27
- .digest("hex");
52
+ async function findThreadByMessageId(messageId, accountId) {
53
+ const session = getSession();
54
+ try {
55
+ const result = await session.run(`MATCH (kd:KnowledgeDocument {source: 'email', accountId: $accountId})
56
+ WHERE $messageId IN kd.messageIds
57
+ RETURN kd.attachmentId AS attachmentId, kd.body AS body,
58
+ kd.messageIds AS messageIds,
59
+ kd.messages AS messagesJson,
60
+ kd.firstReceivedAt AS firstReceivedAt,
61
+ kd.lastReceivedAt AS lastReceivedAt
62
+ LIMIT 1`, { messageId, accountId });
63
+ if (result.records.length === 0)
64
+ return null;
65
+ const record = result.records[0];
66
+ return {
67
+ attachmentId: record.get("attachmentId"),
68
+ body: record.get("body"),
69
+ messageIds: record.get("messageIds"),
70
+ messagesJson: record.get("messagesJson") ?? "[]",
71
+ firstReceivedAt: record.get("firstReceivedAt"),
72
+ lastReceivedAt: record.get("lastReceivedAt"),
73
+ };
74
+ }
75
+ finally {
76
+ await session.close();
28
77
  }
29
- return createHash("sha256")
30
- .update(`uid:${data.uidValidity}:${data.uid}:${data.mailbox}:${data.recipientAddress}`)
31
- .digest("hex");
32
78
  }
33
79
  /**
34
- * Store an email in the graph. Uses MERGE on emailId for idempotent writes.
35
- * Creates the RECEIVED_BY relationship to the EmailAccount in the same transaction.
36
- * Embedding is computed inline if Ollama is down, the email is stored without one.
80
+ * Ingest one IMAP message into the per-thread :KnowledgeDocument.
81
+ *
82
+ * - If the message's In-Reply-To points to a known thread KD, the
83
+ * message is appended (body grows, messageIds grows, lastReceivedAt
84
+ * advances).
85
+ * - Otherwise, a new thread KD is created with this message as root.
37
86
  *
38
- * After storing, creates REPLY_TO edges:
39
- * - If this email has inReplyTo, links to the parent (if it exists in the graph)
40
- * - Back-fills any orphaned emails whose inReplyTo references this email's messageId
87
+ * Person, address-edge, anchor, and embedding writes always run.
88
+ * Discarded-screening messages skip embedding to keep vector search clean.
41
89
  */
42
- export async function storeEmail(data) {
43
- const normalisedMessageId = stripAngleBrackets(data.messageId);
44
- const normalisedInReplyTo = stripAngleBrackets(data.inReplyTo);
45
- const emailId = deriveEmailId({ ...data, messageId: normalisedMessageId });
46
- // Dedup pre-check: when messageId is non-null, check for an existing Email
47
- // with the same messageId + accountId. This catches duplicates caused by
48
- // recipientAddress changes (different emailId hash, same underlying email).
49
- // Null-messageId emails (UID-based fallback) skip this check.
50
- if (normalisedMessageId) {
51
- const checkSession = getSession();
52
- try {
53
- const existing = await checkSession.run(`MATCH (e:Email {messageId: $messageId, accountId: $accountId})
54
- RETURN e.emailId AS emailId LIMIT 1`, { messageId: normalisedMessageId, accountId: data.accountId });
55
- if (existing.records.length > 0) {
56
- const existingEmailId = existing.records[0].get("emailId");
57
- console.error(`[email-fetch] INFO: messageId ${normalisedMessageId} already exists for account ${data.accountId} — skipping (emailId=${existingEmailId})`);
58
- return { emailId: existingEmailId, created: false };
59
- }
60
- }
61
- finally {
62
- await checkSession.close();
63
- }
90
+ export async function storeEmailMessage(data) {
91
+ const messageId = stripAngleBrackets(data.messageId) ?? `synthetic:${deriveFallbackAttachmentId(data)}`;
92
+ const inReplyTo = stripAngleBrackets(data.inReplyTo);
93
+ const existing = inReplyTo ? await findThreadByMessageId(inReplyTo, data.accountId) : null;
94
+ const attachmentId = existing
95
+ ? existing.attachmentId
96
+ : deriveThreadAttachmentId(data.accountId, messageId);
97
+ // Already ingested? Idempotent skip caught by messageIds-contains check.
98
+ if (existing && existing.messageIds.includes(messageId)) {
99
+ return { attachmentId, created: false, appended: false };
64
100
  }
65
- // Embedding computed after pre-check to avoid wasted Ollama calls on duplicates.
66
- // Skip embedding for discarded emails — prevents vector search pollution.
101
+ // Build new body + metadata (append or create).
102
+ const messageBlock = renderMessageBlock(data, messageId);
103
+ const newBody = existing ? `${existing.body}\n\n${messageBlock}` : messageBlock;
104
+ const newMessageIds = existing ? [...existing.messageIds, messageId] : [messageId];
105
+ const newEnvelope = {
106
+ messageId,
107
+ fromAddress: data.fromAddress,
108
+ fromName: data.fromName,
109
+ toAddresses: data.toAddresses,
110
+ ccAddresses: data.ccAddresses,
111
+ subject: data.subject,
112
+ receivedAt: data.receivedAt,
113
+ uid: data.uid,
114
+ mailbox: data.mailbox,
115
+ };
116
+ const existingMessages = existing
117
+ ? JSON.parse(existing.messagesJson)
118
+ : [];
119
+ const newMessages = [...existingMessages, newEnvelope];
120
+ const newLastReceivedAt = existing && data.receivedAt < existing.lastReceivedAt
121
+ ? existing.lastReceivedAt
122
+ : data.receivedAt;
123
+ const newFirstReceivedAt = existing && data.receivedAt > existing.firstReceivedAt
124
+ ? existing.firstReceivedAt
125
+ : data.receivedAt;
126
+ // Embedding: skip discarded messages so vector search stays clean.
67
127
  let embedding = [];
68
128
  if (data.screening !== "discarded") {
69
129
  const embeddingText = [data.subject, data.bodyPreview].filter(Boolean).join(" — ");
70
130
  embedding = await computeEmbedding(embeddingText);
71
131
  }
132
+ const nowIso = new Date().toISOString();
133
+ // Collect addresses for Person/edge MERGE. Sender first so :FROM gets a
134
+ // displayName when only the sender carries one.
135
+ const addressRows = [
136
+ { address: data.fromAddress, displayName: data.fromName, edge: "FROM" },
137
+ ...data.toAddresses.map((address) => ({
138
+ address,
139
+ displayName: null,
140
+ edge: "TO",
141
+ })),
142
+ ...data.ccAddresses.map((address) => ({
143
+ address,
144
+ displayName: null,
145
+ edge: "CC",
146
+ })),
147
+ ]
148
+ .filter((row) => row.address && row.address.length > 0)
149
+ .map((row) => ({ ...row, address: normaliseAddress(row.address) }));
72
150
  const session = getSession();
73
151
  try {
74
- const result = await session.run(`MERGE (e:Email {emailId: $emailId})
152
+ await session.run(`MERGE (kd:KnowledgeDocument {attachmentId: $attachmentId})
75
153
  ON CREATE SET
76
- e.messageId = $messageId,
77
- e.inReplyTo = $inReplyTo,
78
- e.toAddresses = $toAddresses,
79
- e.ccAddresses = $ccAddresses,
80
- e.recipientAddress = $recipientAddress,
81
- e.fromAddress = $fromAddress,
82
- e.fromName = $fromName,
83
- e.subject = $subject,
84
- e.bodyPreview = $bodyPreview,
85
- e.receivedAt = $receivedAt,
86
- e.fetchedAt = $fetchedAt,
87
- e.accountId = $accountId,
88
- e.scope = 'admin',
89
- e.screening = $screening,
90
- e.screeningReason = $screeningReason,
91
- e.promptInjectionFlag = $promptInjectionFlag
92
- WITH e
93
- MATCH (ea:EmailAccount {accountId: $accountId})
94
- MERGE (e)-[:RECEIVED_BY]->(ea)
95
- RETURN e.fetchedAt = $fetchedAt AS created`, {
96
- emailId,
97
- messageId: normalisedMessageId,
98
- inReplyTo: normalisedInReplyTo,
99
- toAddresses: data.toAddresses,
100
- ccAddresses: data.ccAddresses,
101
- recipientAddress: data.recipientAddress,
102
- fromAddress: data.fromAddress,
103
- fromName: data.fromName,
104
- subject: data.subject,
105
- bodyPreview: data.bodyPreview.slice(0, 4096),
106
- receivedAt: data.receivedAt,
107
- fetchedAt: new Date().toISOString(),
154
+ kd.source = 'email',
155
+ kd.scope = 'admin',
156
+ kd.accountId = $accountId,
157
+ kd.subject = $subject,
158
+ kd.createdAt = $nowIso,
159
+ kd.firstReceivedAt = $firstReceivedAt,
160
+ kd.screening = $screening,
161
+ kd.screeningReason = $screeningReason,
162
+ kd.promptInjectionFlag = $promptInjectionFlag
163
+ SET kd.body = $body,
164
+ kd.messageIds = $messageIds,
165
+ kd.messages = $messagesJson,
166
+ kd.lastReceivedAt = $lastReceivedAt,
167
+ kd.updatedAt = $nowIso
168
+ WITH kd
169
+ MERGE (ea:EmailAccount {accountId: $accountId})
170
+ MERGE (kd)-[:RECEIVED_BY]->(ea)`, {
171
+ attachmentId,
108
172
  accountId: data.accountId,
173
+ subject: data.subject,
174
+ body: newBody,
175
+ messageIds: newMessageIds,
176
+ messagesJson: JSON.stringify(newMessages),
177
+ firstReceivedAt: newFirstReceivedAt,
178
+ lastReceivedAt: newLastReceivedAt,
179
+ nowIso,
109
180
  screening: data.screening ?? null,
110
181
  screeningReason: data.screeningReason ?? null,
111
182
  promptInjectionFlag: data.promptInjectionFlag ?? null,
112
183
  });
113
- // Set embedding separately avoids passing a large array through Cypher params
114
- // that could fail on very old Neo4j versions
115
- if (embedding.length > 0) {
116
- await session.run(`MATCH (e:Email {emailId: $emailId})
117
- WHERE e.embedding IS NULL
118
- SET e.embedding = $embedding`, { emailId, embedding });
119
- }
120
- const created = result.records[0]?.get("created") === true;
121
- // Thread linking — best-effort, failures logged but never block email storage
122
- try {
123
- if (normalisedInReplyTo) {
124
- await linkReplyTo(emailId, normalisedInReplyTo, data.accountId);
125
- }
126
- if (normalisedMessageId) {
127
- await backfillOrphans(normalisedMessageId, data.accountId);
128
- }
184
+ // Persons + address edges. UNWIND so one round-trip handles every address.
185
+ if (addressRows.length > 0) {
186
+ await session.run(`UNWIND $rows AS row
187
+ MERGE (p:Person {email: row.address})
188
+ ON CREATE SET p.displayName = row.displayName,
189
+ p.scope = 'admin',
190
+ p.accountId = $accountId,
191
+ p.createdAt = $nowIso
192
+ WITH p, row
193
+ MATCH (kd:KnowledgeDocument {attachmentId: $attachmentId})
194
+ CALL {
195
+ WITH kd, p, row
196
+ WITH kd, p, row WHERE row.edge = 'FROM'
197
+ MERGE (kd)-[:FROM]->(p)
198
+ }
199
+ CALL {
200
+ WITH kd, p, row
201
+ WITH kd, p, row WHERE row.edge = 'TO'
202
+ MERGE (kd)-[:TO]->(p)
203
+ }
204
+ CALL {
205
+ WITH kd, p, row
206
+ WITH kd, p, row WHERE row.edge = 'CC'
207
+ MERGE (kd)-[:CC]->(p)
208
+ }`, { rows: addressRows, attachmentId, accountId: data.accountId, nowIso });
129
209
  }
130
- catch (err) {
131
- console.error(`[email-fetch] WARN: thread linking failed for ${emailId}: ${err instanceof Error ? err.message : String(err)}`);
210
+ // Embedding written separately so the large vector doesn't go through the
211
+ // main MERGE params (some Neo4j versions reject very-large Cypher params).
212
+ if (embedding.length > 0) {
213
+ await session.run(`MATCH (kd:KnowledgeDocument {attachmentId: $attachmentId})
214
+ SET kd.embedding = $embedding`, { attachmentId, embedding });
132
215
  }
133
- return { emailId, created };
216
+ return { attachmentId, created: !existing, appended: !!existing };
134
217
  }
135
218
  catch (err) {
136
- // Constraint violation from email_message_id_account_unique — race condition
137
- // where two concurrent storeEmail calls passed the pre-check before either
138
- // MERGE completed. Treat as a graceful duplicate skip.
139
219
  if (err instanceof neo4j.Neo4jError &&
140
220
  err.code === "Neo.ClientError.Schema.ConstraintValidationFailed") {
141
- console.error(`[email-fetch] INFO: constraint caught duplicate for messageId ${normalisedMessageId} account ${data.accountId} skipping (emailId=${emailId})`);
142
- return { emailId, created: false };
221
+ // Race: two concurrent writes for the same attachmentId. Treat as duplicate.
222
+ console.error(`[email-fetch] INFO: constraint caught duplicate attachmentId=${attachmentId} accountId=${data.accountId}`);
223
+ return { attachmentId, created: false, appended: false };
143
224
  }
144
225
  throw err;
145
226
  }
@@ -148,140 +229,58 @@ export async function storeEmail(data) {
148
229
  }
149
230
  }
150
231
  /**
151
- * Store multiple emails. Returns counts of created and skipped (duplicate) emails.
232
+ * Ingest a batch of IMAP messages. Returns aggregate counts.
233
+ *
234
+ * Threads are intentionally not reordered — messages are processed in
235
+ * input order. Callers that want chronological transcript shape should
236
+ * pre-sort by `receivedAt` ascending.
152
237
  */
153
- export async function storeEmails(emails) {
154
- let created = 0;
155
- let skipped = 0;
156
- for (const email of emails) {
157
- const result = await storeEmail(email);
238
+ export async function storeEmailMessages(messages) {
239
+ let threadsCreated = 0;
240
+ let messagesAppended = 0;
241
+ let duplicates = 0;
242
+ for (const message of messages) {
243
+ const result = await storeEmailMessage(message);
158
244
  if (result.created) {
159
- created++;
245
+ threadsCreated++;
160
246
  }
161
- else {
162
- skipped++;
163
- }
164
- }
165
- return { created, skipped };
166
- }
167
- // ---------------------------------------------------------------------------
168
- // Thread linking
169
- // ---------------------------------------------------------------------------
170
- /**
171
- * Create a REPLY_TO edge from child to parent.
172
- * Looks up the parent Email by messageId within the same account.
173
- * Uses MERGE for idempotency — safe to call multiple times.
174
- */
175
- async function linkReplyTo(childEmailId, inReplyTo, accountId) {
176
- const session = getSession();
177
- try {
178
- const result = await session.run(`MATCH (child:Email {emailId: $childEmailId})
179
- MATCH (parent:Email {messageId: $inReplyTo, accountId: $accountId})
180
- WHERE child <> parent
181
- MERGE (child)-[:REPLY_TO]->(parent)
182
- RETURN parent.emailId AS parentEmailId`, { childEmailId, inReplyTo, accountId });
183
- if (result.records.length > 0) {
184
- const parentId = result.records[0].get("parentEmailId");
185
- console.error(`[email-fetch] INFO: linked REPLY_TO edge: ${childEmailId} → ${parentId}`);
247
+ else if (result.appended) {
248
+ messagesAppended++;
186
249
  }
187
- }
188
- finally {
189
- await session.close();
190
- }
191
- }
192
- /**
193
- * Back-fill REPLY_TO edges for orphaned emails.
194
- * When a new email arrives, check if any existing emails have an inReplyTo
195
- * matching this email's messageId (out-of-order delivery). Create edges
196
- * for any matches within the same account.
197
- */
198
- async function backfillOrphans(messageId, accountId) {
199
- const session = getSession();
200
- try {
201
- const result = await session.run(`MATCH (parent:Email {messageId: $messageId, accountId: $accountId})
202
- MATCH (orphan:Email {inReplyTo: $messageId, accountId: $accountId})
203
- WHERE NOT (orphan)-[:REPLY_TO]->(parent)
204
- AND orphan <> parent
205
- MERGE (orphan)-[:REPLY_TO]->(parent)
206
- RETURN count(orphan) AS linked`, { messageId, accountId });
207
- const linked = neo4j.integer.toNumber(result.records[0]?.get("linked") ?? 0);
208
- if (linked > 0) {
209
- console.error(`[email-fetch] INFO: back-filled ${linked} orphaned REPLY_TO edge(s) for messageId=${messageId}`);
250
+ else {
251
+ duplicates++;
210
252
  }
211
253
  }
212
- finally {
213
- await session.close();
214
- }
254
+ return { threadsCreated, messagesAppended, duplicates };
215
255
  }
216
256
  // ---------------------------------------------------------------------------
217
- // Outbound email storage (auto-respond audit trail)
257
+ // Read helpers — used by email-reply, email-read, email-search to look
258
+ // individual messages and threads back out of the KD model.
218
259
  // ---------------------------------------------------------------------------
219
260
  /**
220
- * Store an outbound auto-respond reply as an Email node.
221
- * Creates the Email node with direction:"outbound" and a REPLY_TO edge
222
- * to the original inbound email. Accepts a Driver parameter because
223
- * email-auto-respond.ts manages its own standalone Neo4j connection.
224
- *
225
- * Embedding is computed inline — if Ollama is down, stored without one.
261
+ * Look up a single message envelope by its Message-ID. Walks the thread
262
+ * KD's `messages` array (JSON-encoded). Returns null when the thread KD
263
+ * has not been ingested yet.
226
264
  */
227
- export async function storeOutboundEmail(data, driver) {
228
- const normalisedMessageId = stripAngleBrackets(data.messageId);
229
- const normalisedInReplyTo = stripAngleBrackets(data.inReplyTo);
230
- // Derive emailId using the outbound messageId
231
- const emailId = createHash("sha256")
232
- .update(`msgid:${normalisedMessageId}:${data.fromAddress}`)
233
- .digest("hex");
234
- const embeddingText = [data.subject, data.bodyPreview].filter(Boolean).join(" — ");
235
- const embedding = await computeEmbedding(embeddingText);
236
- const session = driver.session();
237
- try {
238
- // Create the outbound Email node + REPLY_TO edge to original in one query
239
- await session.run(`MERGE (e:Email {emailId: $emailId})
240
- ON CREATE SET
241
- e.messageId = $messageId,
242
- e.inReplyTo = $inReplyTo,
243
- e.direction = 'outbound',
244
- e.toAddresses = $toAddresses,
245
- e.ccAddresses = [],
246
- e.fromAddress = $fromAddress,
247
- e.subject = $subject,
248
- e.bodyPreview = $bodyPreview,
249
- e.receivedAt = $sentAt,
250
- e.fetchedAt = $sentAt,
251
- e.accountId = $accountId,
252
- e.scope = 'admin'
253
- WITH e
254
- MATCH (original:Email {emailId: $originalEmailId})
255
- MERGE (e)-[:REPLY_TO]->(original)`, {
256
- emailId,
257
- messageId: normalisedMessageId,
258
- inReplyTo: normalisedInReplyTo,
259
- toAddresses: data.toAddresses,
260
- fromAddress: data.fromAddress,
261
- subject: data.subject,
262
- bodyPreview: data.bodyPreview.slice(0, 4096),
263
- sentAt: new Date().toISOString(),
264
- accountId: data.accountId,
265
- originalEmailId: data.originalEmailId,
266
- });
267
- if (embedding.length > 0) {
268
- await session.run(`MATCH (e:Email {emailId: $emailId})
269
- WHERE e.embedding IS NULL
270
- SET e.embedding = $embedding`, { emailId, embedding });
271
- }
272
- return { emailId };
273
- }
274
- finally {
275
- await session.close();
276
- }
265
+ export async function getMessageEnvelope(messageId, accountId) {
266
+ const normalised = stripAngleBrackets(messageId);
267
+ if (!normalised)
268
+ return null;
269
+ const thread = await findThreadByMessageId(normalised, accountId);
270
+ if (!thread)
271
+ return null;
272
+ const messages = JSON.parse(thread.messagesJson);
273
+ return messages.find((m) => m.messageId === normalised) ?? null;
277
274
  }
278
275
  /**
279
- * Batch-query thread info for a set of messageIds.
280
- * For each email, traverses the REPLY_TO chain (capped at 50 hops) to find
281
- * the thread root. Returns threadDepth (number of hops to root) and
282
- * threadId (emailId of the root message).
276
+ * Given a set of messageIds, locate each one inside its thread KD and
277
+ * report:
278
+ * - threadId — the thread KD's attachmentId
279
+ * - threadDepth position of the message in the KD's messageIds array
280
+ * (0 = root). Useful for sorting search hits by thread
281
+ * position.
283
282
  *
284
- * Emails not found in the graph or without REPLY_TO edges return null.
283
+ * Messages whose thread KD has not been ingested are absent from the map.
285
284
  */
286
285
  export async function getThreadInfo(messageIds, accountId) {
287
286
  if (messageIds.length === 0)
@@ -293,18 +292,13 @@ export async function getThreadInfo(messageIds, accountId) {
293
292
  return new Map();
294
293
  const session = getSession();
295
294
  try {
296
- // For each messageId, find the longest REPLY_TO path to a root node
297
- // (a node with no outgoing REPLY_TO edge). Cap at 50 hops.
295
+ // Index-of-mid is computed inline with a list comprehension so the query
296
+ // does not depend on the apoc plugin being installed.
298
297
  const result = await session.run(`UNWIND $messageIds AS mid
299
- MATCH (e:Email {messageId: mid, accountId: $accountId})
300
- OPTIONAL MATCH path = (e)-[:REPLY_TO*1..50]->(ancestor:Email)
301
- WHERE NOT (ancestor)-[:REPLY_TO]->(:Email)
302
- WITH mid, e,
303
- CASE WHEN path IS NULL THEN 0 ELSE length(path) END AS depth,
304
- CASE WHEN path IS NULL THEN e ELSE ancestor END AS root
305
- ORDER BY mid, depth DESC
306
- WITH mid, head(collect(depth)) AS threadDepth, head(collect(root)).emailId AS threadId
307
- WHERE threadDepth > 0
298
+ MATCH (kd:KnowledgeDocument {source: 'email', accountId: $accountId})
299
+ WHERE mid IN kd.messageIds
300
+ WITH mid, kd.attachmentId AS threadId,
301
+ [i IN range(0, size(kd.messageIds) - 1) WHERE kd.messageIds[i] = mid | i][0] AS threadDepth
308
302
  RETURN mid AS messageId, threadDepth, threadId`, { messageIds: normalised, accountId });
309
303
  const map = new Map();
310
304
  for (const record of result.records) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"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"}
@@ -31,7 +31,7 @@ import { resolve } from "node:path";
31
31
  import { homedir } from "node:os";
32
32
  import { loadCredentials, readPollState, updatePollState } from "../lib/credentials.js";
33
33
  import { fetchSinceUid } from "../lib/imap.js";
34
- import { storeEmails } from "../lib/graph.js";
34
+ import { storeEmailMessages } from "../lib/graph.js";
35
35
  import { closeDriver } from "../lib/neo4j.js";
36
36
  import { screenEmail } from "../lib/screening.js";
37
37
  // ---------------------------------------------------------------------------
@@ -209,9 +209,12 @@ async function main() {
209
209
  });
210
210
  }
211
211
  log("INFO", `classification summary: ${countClean} clean, ${countSuspicious} suspicious, ${countDiscard} discarded`);
212
- // Store in graph
213
- const { created, skipped } = await storeEmails(emailData);
214
- log("INFO", `stored ${created} new, ${skipped} duplicates skipped`);
212
+ // Sort by receivedAt ascending so thread transcripts read chronologically
213
+ // (storeEmailMessage appends in input order).
214
+ emailData.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
215
+ // Store in graph as :KnowledgeDocument {source:'email'} per thread.
216
+ const { threadsCreated, messagesAppended, duplicates } = await storeEmailMessages(emailData);
217
+ log("INFO", `stored ${threadsCreated} new threads, ${messagesAppended} messages appended to existing threads, ${duplicates} duplicates skipped`);
215
218
  // Update high-water mark
216
219
  await updatePollState(accountId, maxUid, uidValidity);
217
220
  log("INFO", `high-water mark updated to UID ${maxUid}, uidValidity=${uidValidity}`);