@rubytech/create-maxy-code 0.1.161 → 0.1.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/neo4j/edge-annotations.json +4 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -6
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +12 -15
- package/payload/platform/plugins/admin/mcp/dist/index.js +0 -41
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +8 -12
- package/payload/platform/plugins/email/mcp/dist/index.js +8 -34
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts +39 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js +0 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +23 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +131 -3
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +0 -18
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +0 -12
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +88 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +189 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +6 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +7 -15
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +206 -86
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +104 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +18 -12
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +5 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +13 -12
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +8 -8
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +1 -13
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +3 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +10 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +5 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +6 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -1
- package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
- package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
- package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
- package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -4
- package/payload/platform/scripts/seed-neo4j.sh +1 -3
- package/payload/platform/templates/specialists/agents/librarian.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -361
- package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
- package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -106
- package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
|
@@ -2,109 +2,182 @@ import neo4j from "neo4j-driver";
|
|
|
2
2
|
import { getSession } from "../lib/neo4j.js";
|
|
3
3
|
import { computeEmbedding } from "../lib/embedding.js";
|
|
4
4
|
/**
|
|
5
|
-
* Query email-
|
|
5
|
+
* Query stored email-thread `:ConversationArchive` rows in the graph.
|
|
6
6
|
*
|
|
7
7
|
* Two modes:
|
|
8
|
-
* - List (no query):
|
|
9
|
-
*
|
|
8
|
+
* - List (no query): MATCH archives with optional sender + date filters,
|
|
9
|
+
* sorted by lastMessageAt DESC.
|
|
10
|
+
* - Search (query provided): embed via Ollama, run vector similarity over
|
|
11
|
+
* `:Section` chunks, dedup to parent archives, rank by best section.
|
|
10
12
|
*
|
|
11
|
-
* Always scoped to accountId. The display surface is
|
|
12
|
-
* per-message
|
|
13
|
+
* Always scoped to (accountId, source='email'). The display surface is
|
|
14
|
+
* per-archive; per-message envelopes are not stored — Task 445.
|
|
13
15
|
*/
|
|
14
16
|
export async function emailGraphQuery(params) {
|
|
15
17
|
const limit = Math.min(Math.max(1, params.limit ?? 20), 50);
|
|
16
18
|
const offset = Math.max(0, params.offset ?? 0);
|
|
17
19
|
const query = params.query?.trim();
|
|
18
|
-
const
|
|
20
|
+
const archives = query && query.length > 0
|
|
19
21
|
? await searchMode(query, params, limit)
|
|
20
22
|
: await listMode(params, limit, offset);
|
|
21
|
-
if (
|
|
23
|
+
if (archives.length === 0) {
|
|
22
24
|
return formatEmptyResult(params, query);
|
|
23
25
|
}
|
|
24
|
-
const lines =
|
|
25
|
-
.map((
|
|
26
|
-
const
|
|
27
|
-
?
|
|
28
|
-
:
|
|
29
|
-
let entry = `
|
|
30
|
-
entry += `\
|
|
31
|
-
entry += `\
|
|
32
|
-
entry += `\
|
|
33
|
-
entry += `\nFirst: ${
|
|
34
|
-
entry += `\nLast: ${
|
|
35
|
-
if (
|
|
36
|
-
entry += `\
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const preview = t.body.length > 800 ? `${t.body.slice(0, 800)}\n…(truncated)` : t.body;
|
|
41
|
-
entry += `\n\n${preview}`;
|
|
26
|
+
const lines = archives
|
|
27
|
+
.map((a) => {
|
|
28
|
+
const participants = a.participantNames.length > 0
|
|
29
|
+
? a.participantNames.join(", ")
|
|
30
|
+
: "(unknown)";
|
|
31
|
+
let entry = `Archive: ${a.elementId.slice(0, 16)}…`;
|
|
32
|
+
entry += `\nTitle: ${a.title || "(untitled)"}`;
|
|
33
|
+
entry += `\nParticipants: ${participants}`;
|
|
34
|
+
entry += `\nSections: ${a.sectionCount}`;
|
|
35
|
+
entry += `\nFirst: ${a.firstMessageAt}`;
|
|
36
|
+
entry += `\nLast: ${a.lastMessageAt}`;
|
|
37
|
+
if (a.score !== undefined)
|
|
38
|
+
entry += `\nScore: ${a.score.toFixed(3)}`;
|
|
39
|
+
const preview = a.summary.length > 800 ? `${a.summary.slice(0, 800)}\n…(truncated)` : a.summary;
|
|
40
|
+
if (preview)
|
|
41
|
+
entry += `\n\n${preview}`;
|
|
42
42
|
return entry + "\n---";
|
|
43
43
|
})
|
|
44
44
|
.join("\n\n");
|
|
45
|
-
if (!query &&
|
|
45
|
+
if (!query && archives.length === limit) {
|
|
46
46
|
return `${lines}\n\nPagination: next offset=${offset + limit} (pass as offset to get more results)`;
|
|
47
47
|
}
|
|
48
48
|
return lines;
|
|
49
49
|
}
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
|
-
// List mode — Cypher MATCH with optional filters
|
|
51
|
+
// List mode — Cypher MATCH on :ConversationArchive with optional filters
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
async function listMode(params, limit, offset) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"kd.source = 'email'",
|
|
58
|
-
"kd.accountId = $accountId",
|
|
59
|
-
"(kd.screening <> 'discarded' OR kd.screening IS NULL)",
|
|
54
|
+
const archiveWhere = [
|
|
55
|
+
"a.source = 'email'",
|
|
56
|
+
"a.accountId = $accountId",
|
|
60
57
|
];
|
|
61
58
|
const cyParams = {
|
|
62
59
|
accountId: params.accountId,
|
|
63
60
|
limit: neo4j.int(limit),
|
|
64
61
|
offset: neo4j.int(offset),
|
|
65
62
|
};
|
|
63
|
+
// Date filters apply at the section level (firstMessageAt/lastMessageAt
|
|
64
|
+
// span the archive). Carry the archive only when at least one section
|
|
65
|
+
// overlaps the requested window.
|
|
66
|
+
let sectionDateFilter = "";
|
|
67
|
+
const sectionDateConds = [];
|
|
66
68
|
if (params.dateFrom) {
|
|
67
|
-
|
|
69
|
+
sectionDateConds.push("s.lastMessageAt >= $dateFrom");
|
|
68
70
|
cyParams.dateFrom = params.dateFrom;
|
|
69
71
|
}
|
|
70
72
|
if (params.dateTo) {
|
|
71
|
-
|
|
73
|
+
sectionDateConds.push("s.firstMessageAt <= $dateTo");
|
|
72
74
|
cyParams.dateTo = params.dateTo;
|
|
73
75
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
+
if (sectionDateConds.length > 0) {
|
|
77
|
+
sectionDateFilter = `WHERE ${sectionDateConds.join(" AND ")}`;
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const senderClause = params.sender
|
|
81
|
-
? `, (kd)-[:FROM]->(sender:Person)
|
|
82
|
-
WHERE toLower(sender.email) CONTAINS toLower($sender)`
|
|
83
|
-
: `OPTIONAL MATCH (kd)-[:FROM]->(sender:Person)`;
|
|
79
|
+
// Sender filter joins through :PARTICIPANT_IN; matches Person OR AdminUser
|
|
80
|
+
// on case-insensitive email substring.
|
|
81
|
+
let senderFilter = "";
|
|
84
82
|
if (params.sender) {
|
|
83
|
+
senderFilter = `
|
|
84
|
+
WITH a
|
|
85
|
+
MATCH (p)-[:PARTICIPANT_IN]->(a)
|
|
86
|
+
WHERE (p:Person OR p:AdminUser)
|
|
87
|
+
AND p.email IS NOT NULL
|
|
88
|
+
AND toLower(p.email) CONTAINS toLower($sender)
|
|
89
|
+
`;
|
|
85
90
|
cyParams.sender = params.sender;
|
|
86
91
|
}
|
|
87
92
|
const cypher = `
|
|
88
|
-
MATCH (
|
|
89
|
-
${
|
|
90
|
-
|
|
91
|
-
WITH
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
MATCH (a:ConversationArchive)
|
|
94
|
+
WHERE ${archiveWhere.join(" AND ")}
|
|
95
|
+
${senderFilter}
|
|
96
|
+
WITH DISTINCT a
|
|
97
|
+
OPTIONAL MATCH (a)-[:HAS_SECTION]->(s:Section)
|
|
98
|
+
${sectionDateFilter}
|
|
99
|
+
WITH a, collect(s) AS matchedSections
|
|
100
|
+
WHERE size(matchedSections) > 0 OR ($dateFrom IS NULL AND $dateTo IS NULL)
|
|
101
|
+
OPTIONAL MATCH (a)-[:HAS_SECTION]->(allS:Section)
|
|
102
|
+
WITH a, matchedSections, allS
|
|
103
|
+
ORDER BY coalesce(allS.position, 0) ASC
|
|
104
|
+
WITH a,
|
|
105
|
+
matchedSections,
|
|
106
|
+
collect(allS) AS allSections
|
|
107
|
+
WITH a,
|
|
108
|
+
allSections,
|
|
109
|
+
[s IN allSections | coalesce(s.summary, '')] AS summaries,
|
|
110
|
+
[s IN allSections WHERE s.firstMessageAt IS NOT NULL | s.firstMessageAt] AS firsts,
|
|
111
|
+
[s IN allSections WHERE s.lastMessageAt IS NOT NULL | s.lastMessageAt] AS lasts
|
|
112
|
+
OPTIONAL MATCH (p)-[:PARTICIPANT_IN]->(a)
|
|
113
|
+
WHERE p:Person OR p:AdminUser
|
|
114
|
+
WITH a,
|
|
115
|
+
allSections,
|
|
116
|
+
summaries,
|
|
117
|
+
firsts,
|
|
118
|
+
lasts,
|
|
119
|
+
collect(DISTINCT coalesce(p.displayName, p.name, p.email, '')) AS participantNames
|
|
120
|
+
RETURN elementId(a) AS elementId,
|
|
121
|
+
coalesce(a.title, '') AS title,
|
|
122
|
+
[x IN summaries WHERE x <> ''] AS summaryParts,
|
|
123
|
+
CASE WHEN size(firsts) > 0 THEN apoc.coll.min(firsts) ELSE coalesce(a.createdAt, '') END AS firstMessageAt,
|
|
124
|
+
CASE WHEN size(lasts) > 0 THEN apoc.coll.max(lasts) ELSE coalesce(a.lastIngestedMessageAt, a.updatedAt, '') END AS lastMessageAt,
|
|
125
|
+
size(allSections) AS sectionCount,
|
|
126
|
+
[n IN participantNames WHERE n <> ''] AS participantNames
|
|
127
|
+
ORDER BY lastMessageAt DESC
|
|
128
|
+
SKIP $offset
|
|
129
|
+
LIMIT $limit
|
|
130
|
+
`;
|
|
131
|
+
// Fallback when APOC isn't available: compute min/max in JS. Strip the
|
|
132
|
+
// apoc.* calls and pull raw arrays instead.
|
|
133
|
+
const cypherNoApoc = `
|
|
134
|
+
MATCH (a:ConversationArchive)
|
|
135
|
+
WHERE ${archiveWhere.join(" AND ")}
|
|
136
|
+
${senderFilter}
|
|
137
|
+
WITH DISTINCT a
|
|
138
|
+
OPTIONAL MATCH (a)-[:HAS_SECTION]->(s:Section)
|
|
139
|
+
${sectionDateFilter}
|
|
140
|
+
WITH a, collect(s) AS matchedSections
|
|
141
|
+
WHERE size(matchedSections) > 0 OR ($dateFrom IS NULL AND $dateTo IS NULL)
|
|
142
|
+
OPTIONAL MATCH (a)-[:HAS_SECTION]->(allS:Section)
|
|
143
|
+
WITH a, allS
|
|
144
|
+
ORDER BY coalesce(allS.position, 0) ASC
|
|
145
|
+
WITH a, collect(allS) AS allSections
|
|
146
|
+
OPTIONAL MATCH (p)-[:PARTICIPANT_IN]->(a)
|
|
147
|
+
WHERE p:Person OR p:AdminUser
|
|
148
|
+
WITH a,
|
|
149
|
+
allSections,
|
|
150
|
+
collect(DISTINCT coalesce(p.displayName, p.name, p.email, '')) AS participantNames
|
|
151
|
+
RETURN elementId(a) AS elementId,
|
|
152
|
+
coalesce(a.title, '') AS title,
|
|
153
|
+
[x IN allSections | coalesce(x.summary, '')] AS summaryParts,
|
|
154
|
+
[x IN allSections WHERE x.firstMessageAt IS NOT NULL | x.firstMessageAt] AS firsts,
|
|
155
|
+
[x IN allSections WHERE x.lastMessageAt IS NOT NULL | x.lastMessageAt] AS lasts,
|
|
156
|
+
coalesce(a.createdAt, '') AS archiveCreatedAt,
|
|
157
|
+
coalesce(a.lastIngestedMessageAt, a.updatedAt, '') AS archiveLastAt,
|
|
158
|
+
size(allSections) AS sectionCount,
|
|
159
|
+
[n IN participantNames WHERE n <> ''] AS participantNames
|
|
160
|
+
ORDER BY archiveLastAt DESC
|
|
94
161
|
SKIP $offset
|
|
95
162
|
LIMIT $limit
|
|
96
163
|
`;
|
|
164
|
+
// dateFrom/dateTo must always be bound (Cypher requires named params even
|
|
165
|
+
// for the no-filter branch above where the WHERE references them).
|
|
166
|
+
if (cyParams.dateFrom === undefined)
|
|
167
|
+
cyParams.dateFrom = null;
|
|
168
|
+
if (cyParams.dateTo === undefined)
|
|
169
|
+
cyParams.dateTo = null;
|
|
97
170
|
const session = getSession();
|
|
98
171
|
try {
|
|
99
|
-
const result = await session.run(
|
|
100
|
-
return result.records.map(
|
|
172
|
+
const result = await session.run(cypherNoApoc, cyParams);
|
|
173
|
+
return result.records.map(recordToArchive);
|
|
101
174
|
}
|
|
102
175
|
finally {
|
|
103
176
|
await session.close();
|
|
104
177
|
}
|
|
105
178
|
}
|
|
106
179
|
// ---------------------------------------------------------------------------
|
|
107
|
-
// Search mode — vector similarity
|
|
180
|
+
// Search mode — vector similarity over :Section, dedup to parent archive
|
|
108
181
|
// ---------------------------------------------------------------------------
|
|
109
182
|
async function searchMode(query, params, limit) {
|
|
110
183
|
const embedding = await computeEmbedding(query);
|
|
@@ -112,26 +185,62 @@ async function searchMode(query, params, limit) {
|
|
|
112
185
|
throw new Error("Semantic search unavailable — the embedding service is not responding. " +
|
|
113
186
|
"Try again without a query to list email threads instead.");
|
|
114
187
|
}
|
|
115
|
-
const topK = Math.min(limit *
|
|
188
|
+
const topK = Math.min(limit * 6, 300);
|
|
189
|
+
const cyParams = {
|
|
190
|
+
topK: neo4j.int(topK),
|
|
191
|
+
embedding,
|
|
192
|
+
accountId: params.accountId,
|
|
193
|
+
limit: neo4j.int(limit),
|
|
194
|
+
sender: params.sender ?? null,
|
|
195
|
+
dateFrom: params.dateFrom ?? null,
|
|
196
|
+
dateTo: params.dateTo ?? null,
|
|
197
|
+
};
|
|
198
|
+
// Section-level vector search → restrict to email archives → keep best
|
|
199
|
+
// section per archive → carry that score as the archive's rank. Sender
|
|
200
|
+
// and date filters are applied post-vector (cheaper than pre-filtering
|
|
201
|
+
// a billion-row index, and the topK over-fetch absorbs the loss).
|
|
202
|
+
const cypher = `
|
|
203
|
+
CALL db.index.vector.queryNodes('section_embedding', $topK, $embedding)
|
|
204
|
+
YIELD node, score
|
|
205
|
+
WITH node, score
|
|
206
|
+
MATCH (a:ConversationArchive { accountId: $accountId, source: 'email' })
|
|
207
|
+
-[:HAS_SECTION]->(node)
|
|
208
|
+
WHERE ($dateFrom IS NULL OR node.lastMessageAt >= $dateFrom)
|
|
209
|
+
AND ($dateTo IS NULL OR node.firstMessageAt <= $dateTo)
|
|
210
|
+
WITH a, score
|
|
211
|
+
ORDER BY score DESC
|
|
212
|
+
WITH a, head(collect(score)) AS bestScore
|
|
213
|
+
OPTIONAL MATCH (p)-[:PARTICIPANT_IN]->(a)
|
|
214
|
+
WHERE p:Person OR p:AdminUser
|
|
215
|
+
WITH a, bestScore,
|
|
216
|
+
collect(DISTINCT coalesce(p.displayName, p.name, p.email, '')) AS participantNames,
|
|
217
|
+
collect(DISTINCT toLower(coalesce(p.email, ''))) AS participantEmails
|
|
218
|
+
WHERE $sender IS NULL
|
|
219
|
+
OR any(e IN participantEmails WHERE e CONTAINS toLower($sender))
|
|
220
|
+
OPTIONAL MATCH (a)-[:HAS_SECTION]->(allS:Section)
|
|
221
|
+
WITH a, bestScore, participantNames, allS
|
|
222
|
+
ORDER BY coalesce(allS.position, 0) ASC
|
|
223
|
+
WITH a, bestScore, participantNames, collect(allS) AS allSections
|
|
224
|
+
RETURN elementId(a) AS elementId,
|
|
225
|
+
coalesce(a.title, '') AS title,
|
|
226
|
+
[x IN allSections | coalesce(x.summary, '')] AS summaryParts,
|
|
227
|
+
[x IN allSections WHERE x.firstMessageAt IS NOT NULL | x.firstMessageAt] AS firsts,
|
|
228
|
+
[x IN allSections WHERE x.lastMessageAt IS NOT NULL | x.lastMessageAt] AS lasts,
|
|
229
|
+
coalesce(a.createdAt, '') AS archiveCreatedAt,
|
|
230
|
+
coalesce(a.lastIngestedMessageAt, a.updatedAt, '') AS archiveLastAt,
|
|
231
|
+
size(allSections) AS sectionCount,
|
|
232
|
+
[n IN participantNames WHERE n <> ''] AS participantNames,
|
|
233
|
+
bestScore AS score
|
|
234
|
+
ORDER BY score DESC
|
|
235
|
+
LIMIT $limit
|
|
236
|
+
`;
|
|
116
237
|
const session = getSession();
|
|
117
238
|
try {
|
|
118
|
-
const result = await session.run(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
RETURN node, score, rootSender
|
|
124
|
-
ORDER BY score DESC
|
|
125
|
-
LIMIT $limit`, {
|
|
126
|
-
topK: neo4j.int(topK),
|
|
127
|
-
embedding,
|
|
128
|
-
accountId: params.accountId,
|
|
129
|
-
limit: neo4j.int(limit),
|
|
130
|
-
});
|
|
131
|
-
return result.records.map((r) => {
|
|
132
|
-
const record = nodeToRecord(r.get("node"), r.get("rootSender"));
|
|
133
|
-
record.score = r.get("score");
|
|
134
|
-
return record;
|
|
239
|
+
const result = await session.run(cypher, cyParams);
|
|
240
|
+
return result.records.map((rec) => {
|
|
241
|
+
const archive = recordToArchive(rec);
|
|
242
|
+
archive.score = rec.get("score");
|
|
243
|
+
return archive;
|
|
135
244
|
});
|
|
136
245
|
}
|
|
137
246
|
finally {
|
|
@@ -141,19 +250,32 @@ async function searchMode(query, params, limit) {
|
|
|
141
250
|
// ---------------------------------------------------------------------------
|
|
142
251
|
// Helpers
|
|
143
252
|
// ---------------------------------------------------------------------------
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
253
|
+
function toNumber(v) {
|
|
254
|
+
if (typeof v === "number")
|
|
255
|
+
return v;
|
|
256
|
+
if (typeof v?.toNumber === "function") {
|
|
257
|
+
return v.toNumber();
|
|
258
|
+
}
|
|
259
|
+
return Number(v ?? 0);
|
|
260
|
+
}
|
|
261
|
+
function recordToArchive(rec) {
|
|
262
|
+
const summaryParts = rec.get("summaryParts") ?? [];
|
|
263
|
+
const firsts = rec.get("firsts") ?? [];
|
|
264
|
+
const lasts = rec.get("lasts") ?? [];
|
|
265
|
+
const firstMessageAt = firsts.length > 0
|
|
266
|
+
? firsts.slice().sort()[0]
|
|
267
|
+
: rec.get("archiveCreatedAt") ?? "";
|
|
268
|
+
const lastMessageAt = lasts.length > 0
|
|
269
|
+
? lasts.slice().sort()[lasts.length - 1]
|
|
270
|
+
: rec.get("archiveLastAt") ?? "";
|
|
147
271
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
rootFromAddress: sp ? (sp.email ?? null) : null,
|
|
156
|
-
rootFromName: sp ? (sp.displayName ?? null) : null,
|
|
272
|
+
elementId: rec.get("elementId"),
|
|
273
|
+
title: rec.get("title") ?? "",
|
|
274
|
+
summary: summaryParts.filter((s) => s).join("\n\n"),
|
|
275
|
+
firstMessageAt,
|
|
276
|
+
lastMessageAt,
|
|
277
|
+
sectionCount: toNumber(rec.get("sectionCount")),
|
|
278
|
+
participantNames: rec.get("participantNames") ?? [],
|
|
157
279
|
};
|
|
158
280
|
}
|
|
159
281
|
function formatEmptyResult(params, query) {
|
|
@@ -166,8 +288,6 @@ function formatEmptyResult(params, query) {
|
|
|
166
288
|
filters.push(`from: ${params.dateFrom}`);
|
|
167
289
|
if (params.dateTo)
|
|
168
290
|
filters.push(`to: ${params.dateTo}`);
|
|
169
|
-
|
|
170
|
-
filters.push(`direction: ${params.direction}`);
|
|
171
|
-
return `No email threads found in the graph${filters.length > 0 ? ` matching ${filters.join(", ")}` : ""}.`;
|
|
291
|
+
return `No email archives found in the graph${filters.length > 0 ? ` matching ${filters.join(", ")}` : ""}.`;
|
|
172
292
|
}
|
|
173
293
|
//# sourceMappingURL=email-graph-query.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-graph-query.js","sourceRoot":"","sources":["../../src/tools/email-graph-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,cAAc,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAoCvD
|
|
1
|
+
{"version":3,"file":"email-graph-query.js","sourceRoot":"","sources":["../../src/tools/email-graph-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-ingest.d.ts","sourceRoot":"","sources":["../../src/tools/email-ingest.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"email-ingest.d.ts","sourceRoot":"","sources":["../../src/tools/email-ingest.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,QAAQ,GAAG,SAAS,CAAC;CACnC;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAMD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CA4GvE"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { updatePollState } from "../lib/credentials.js";
|
|
1
|
+
import { loadCredentials, updatePollState } from "../lib/credentials.js";
|
|
2
2
|
import { stripAngleBrackets } from "../lib/graph.js";
|
|
3
|
-
import { dispatchEmailMessagesToConversationArchive } from "../lib/conversation-archive-dispatch.js";
|
|
3
|
+
import { dispatchEmailMessagesToConversationArchive, } from "../lib/conversation-archive-dispatch.js";
|
|
4
4
|
import { takeBatch, peekBatch } from "../lib/ingest-batch.js";
|
|
5
|
+
import { fetchAttachmentBytesForMessages } from "../lib/imap.js";
|
|
6
|
+
import { archiveAttachment, sha256OfBytes, syntheticAttachmentId, } from "../lib/attachment-archive.js";
|
|
5
7
|
function normaliseId(id) {
|
|
6
8
|
return stripAngleBrackets(id)?.toLowerCase() ?? null;
|
|
7
9
|
}
|
|
@@ -72,15 +74,30 @@ export async function emailIngest(params) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
toIngest.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
|
|
75
|
-
|
|
77
|
+
// ─── Attachment byte fetch + on-disk archive (Task 402) ───────────────
|
|
78
|
+
//
|
|
79
|
+
// For every approved message, look up its attachment metadata in the
|
|
80
|
+
// cached batch, fetch the bytes from IMAP in one connection, archive
|
|
81
|
+
// them to disk, and build the per-attachment records the dispatcher
|
|
82
|
+
// needs to MERGE :DigitalDocument nodes.
|
|
83
|
+
const ingestedUids = new Set(toIngest.map((m) => m.uid));
|
|
84
|
+
const metaByUid = new Map();
|
|
85
|
+
for (const m of batch.messages) {
|
|
86
|
+
if (ingestedUids.has(m.uid) && m.attachments.length > 0) {
|
|
87
|
+
metaByUid.set(m.uid, m.attachments);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const attachmentRecords = await buildAttachmentRecords(params.accountId, batch.uidValidity, metaByUid);
|
|
91
|
+
const { threadsDispatched, threadsSkipped, unresolvedAddresses, attachmentsAttached, attachmentsFailed } = await dispatchEmailMessagesToConversationArchive(toIngest, attachmentRecords);
|
|
76
92
|
// Advance the high-water mark so the next email-fetch starts after this batch.
|
|
77
93
|
await updatePollState(params.accountId, batch.maxUid, batch.uidValidity);
|
|
78
94
|
// Consume the cached batch only after a successful write + state advance.
|
|
79
95
|
takeBatch(params.accountId);
|
|
80
|
-
console.error(`[email-ingest] tag=email-ingest ingested=${toIngest.length} discarded=${discarded} threadsDispatched=${threadsDispatched} threadsSkipped=${threadsSkipped} unresolved=${unresolvedAddresses.length}`);
|
|
96
|
+
console.error(`[email-ingest] tag=email-ingest ingested=${toIngest.length} discarded=${discarded} threadsDispatched=${threadsDispatched} threadsSkipped=${threadsSkipped} unresolved=${unresolvedAddresses.length} attachmentsAttached=${attachmentsAttached} attachmentsFailed=${attachmentsFailed}`);
|
|
81
97
|
const lines = [
|
|
82
98
|
`Ingested ${toIngest.length} message(s); discarded ${discarded}.`,
|
|
83
99
|
`Graph: ${threadsDispatched} thread(s) dispatched to conversation-archive, ${threadsSkipped} skipped.`,
|
|
100
|
+
`Attachments: ${attachmentsAttached} archived as :DigitalDocument, ${attachmentsFailed} failed.`,
|
|
84
101
|
`Inbox high-water mark advanced to UID ${batch.maxUid}.`,
|
|
85
102
|
];
|
|
86
103
|
if (unresolvedAddresses.length > 0) {
|
|
@@ -88,4 +105,87 @@ export async function emailIngest(params) {
|
|
|
88
105
|
}
|
|
89
106
|
return lines.join("\n");
|
|
90
107
|
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Attachment byte fetch + on-disk archive (Task 402).
|
|
110
|
+
//
|
|
111
|
+
// metaByUid maps approved-message UID → the attachment metadata extracted at
|
|
112
|
+
// email-fetch time (filename, mimeType, sizeBytes, partKey). For each
|
|
113
|
+
// metadata entry we either fetch bytes and archive to disk, or skip bytes
|
|
114
|
+
// (oversize / fetch-error) and write a synthetic-id record so the
|
|
115
|
+
// :DigitalDocument still lands with metadata-only.
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
async function buildAttachmentRecords(accountId, uidValidity, metaByUid) {
|
|
118
|
+
if (metaByUid.size === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const accountDir = process.env.ACCOUNT_DIR;
|
|
121
|
+
if (!accountDir) {
|
|
122
|
+
console.error("[email-attach] WARN: ACCOUNT_DIR env not set; attachment bytes will not be archived (metadata-only :DigitalDocument nodes will still land).");
|
|
123
|
+
}
|
|
124
|
+
const credResult = await loadCredentials(accountId);
|
|
125
|
+
if ("error" in credResult) {
|
|
126
|
+
console.error(`[email-attach] WARN: loadCredentials failed during attachment fetch: ${credResult.error}`);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const { config, password } = credResult.credentials;
|
|
130
|
+
// Flatten the (uid, partKey) requests across all approved messages.
|
|
131
|
+
const requests = [];
|
|
132
|
+
for (const [uid, metas] of metaByUid.entries()) {
|
|
133
|
+
for (const m of metas) {
|
|
134
|
+
requests.push({ uid, partKey: m.partKey, expectedSize: m.sizeBytes, encoding: m.encoding, meta: m });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const fetched = await fetchAttachmentBytesForMessages(config, password, requests.map(({ uid, partKey, expectedSize, encoding }) => ({ uid, partKey, expectedSize, encoding })));
|
|
138
|
+
// Index by (uid, partKey) for O(1) lookup.
|
|
139
|
+
const fetchedKey = (uid, partKey) => `${uid}:${partKey}`;
|
|
140
|
+
const fetchedByKey = new Map(fetched.map((f) => [fetchedKey(f.uid, f.partKey), f]));
|
|
141
|
+
const records = [];
|
|
142
|
+
for (const req of requests) {
|
|
143
|
+
const f = fetchedByKey.get(fetchedKey(req.uid, req.partKey));
|
|
144
|
+
if (!f)
|
|
145
|
+
continue;
|
|
146
|
+
if (f.bytes === null) {
|
|
147
|
+
// Bytes not available — write a metadata-only record with a
|
|
148
|
+
// synthetic content-free attachmentId so the graph node still lands.
|
|
149
|
+
console.error(`[email-attach] metadata-only uid=${req.uid} part=${req.partKey} filename=${req.meta.filename} reason=${f.failureReason}`);
|
|
150
|
+
records.push({
|
|
151
|
+
uid: req.uid,
|
|
152
|
+
partKey: req.partKey,
|
|
153
|
+
filename: req.meta.filename,
|
|
154
|
+
mimeType: req.meta.mimeType,
|
|
155
|
+
sizeBytes: req.meta.sizeBytes,
|
|
156
|
+
attachmentId: syntheticAttachmentId(uidValidity, req.uid, req.partKey, req.meta.filename, req.meta.sizeBytes),
|
|
157
|
+
archivePath: null,
|
|
158
|
+
archived: false,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Bytes in hand — archive to disk (best-effort) and use sha256(bytes)
|
|
163
|
+
// as the content-addressed attachmentId regardless of archive outcome.
|
|
164
|
+
const attachmentId = sha256OfBytes(f.bytes);
|
|
165
|
+
let archivePath = null;
|
|
166
|
+
let archived = false;
|
|
167
|
+
if (accountDir) {
|
|
168
|
+
const result = archiveAttachment(accountDir, uidValidity, req.uid, req.meta.filename, f.bytes);
|
|
169
|
+
if (result) {
|
|
170
|
+
archivePath = result.archivePath;
|
|
171
|
+
archived = true;
|
|
172
|
+
console.error(`[email-attach] archived uid=${req.uid} part=${req.partKey} sha256=${attachmentId.slice(0, 12)} path=${archivePath}`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.error(`[email-attach] archive-skipped uid=${req.uid} part=${req.partKey} (write failed; node will still land with archivePath=null)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
records.push({
|
|
179
|
+
uid: req.uid,
|
|
180
|
+
partKey: req.partKey,
|
|
181
|
+
filename: req.meta.filename,
|
|
182
|
+
mimeType: req.meta.mimeType,
|
|
183
|
+
sizeBytes: req.meta.sizeBytes,
|
|
184
|
+
attachmentId,
|
|
185
|
+
archivePath,
|
|
186
|
+
archived,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return records;
|
|
190
|
+
}
|
|
91
191
|
//# sourceMappingURL=email-ingest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-ingest.js","sourceRoot":"","sources":["../../src/tools/email-ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"email-ingest.js","sourceRoot":"","sources":["../../src/tools/email-ingest.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-read.d.ts","sourceRoot":"","sources":["../../src/tools/email-read.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"email-read.d.ts","sourceRoot":"","sources":["../../src/tools/email-read.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA0EnE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadCredentials } from "../lib/credentials.js";
|
|
2
2
|
import { fetchRecent, MAX_LIMIT } from "../lib/imap.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getMessageArchiveHints } from "../lib/conversation-archive-lookup.js";
|
|
4
4
|
export async function emailRead(params) {
|
|
5
5
|
const result = await loadCredentials(params.accountId);
|
|
6
6
|
if ("error" in result)
|
|
@@ -38,14 +38,21 @@ export async function emailRead(params) {
|
|
|
38
38
|
filters.push(`folder: ${params.folder}`);
|
|
39
39
|
return `No messages found${filters.length > 0 ? ` matching ${filters.join(", ")}` : ""}.`;
|
|
40
40
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Per-archive enrichment from :ConversationArchive {source:'email'}.
|
|
42
|
+
// Best-effort — failures and empty results degrade silently. Matches on
|
|
43
|
+
// (accountId, receivedAt within :Section.firstMessageAt..lastMessageAt)
|
|
44
|
+
// and surfaces the parent archive's title + the matching section's
|
|
45
|
+
// ordered position. Semantics shift from Task-321 per-message thread
|
|
46
|
+
// depth to per-archive ordering — see Task 445.
|
|
47
|
+
const hintInput = messages
|
|
48
|
+
.filter((m) => m.messageId)
|
|
49
|
+
.map((m) => ({ messageId: m.messageId, receivedAt: m.date }));
|
|
50
|
+
let hints = new Map();
|
|
44
51
|
try {
|
|
45
|
-
|
|
52
|
+
hints = await getMessageArchiveHints(hintInput, params.accountId);
|
|
46
53
|
}
|
|
47
54
|
catch {
|
|
48
|
-
//
|
|
55
|
+
// Enrichment is optional — continue without it
|
|
49
56
|
}
|
|
50
57
|
const lines = messages
|
|
51
58
|
.map((m) => {
|
|
@@ -54,12 +61,11 @@ export async function emailRead(params) {
|
|
|
54
61
|
entry += `\nMessage-ID: ${m.messageId}`;
|
|
55
62
|
if (m.inReplyTo)
|
|
56
63
|
entry += `\nIn-Reply-To: ${m.inReplyTo}`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
entry += `\
|
|
62
|
-
entry += `\nThread-ID: ${thread.threadId}`;
|
|
64
|
+
if (m.messageId) {
|
|
65
|
+
const hint = hints.get(m.messageId);
|
|
66
|
+
if (hint) {
|
|
67
|
+
entry += `\nArchive: ${hint.archiveTitle}`;
|
|
68
|
+
entry += `\nArchive-Section: ${hint.sectionIndex}`;
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
if (m.bodyPreview)
|