@rubytech/create-maxy-code 0.1.159 → 0.1.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +48 -17
- package/payload/platform/neo4j/schema.cypher +59 -36
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/index.js +25 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +8 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +177 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +6 -54
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +12 -300
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts +6 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js.map +1 -0
- 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 +11 -6
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +10 -37
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -0
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +10 -1
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +139 -439
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +15 -47
- package/payload/platform/plugins/memory/mcp/dist/index.js +168 -29
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +43 -27
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +42 -25
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +23 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js +61 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +9 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +71 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js +31 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +162 -78
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +33 -34
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +94 -273
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js +96 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js +73 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +40 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +7 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +17 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js +46 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js.map +1 -0
- 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 +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +5 -4
- package/payload/platform/plugins/memory/references/schema-base.md +4 -6
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +100 -116
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +74 -24
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
- package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/substack-import/PLUGIN.md +34 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +182 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/archive-shape.md +68 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/attachments.md +72 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/engagement.md +61 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +80 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/subscribers.md +74 -0
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
- package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +45 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -17
- package/payload/platform/templates/specialists/agents/librarian.md +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { getSession } from "./neo4j.js";
|
|
7
|
+
import { stripAngleBrackets } from "./graph.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// conversation-archive-dispatch — Task 426.
|
|
10
|
+
//
|
|
11
|
+
// Hands a batch of operator-approved IMAP messages to the source-agnostic
|
|
12
|
+
// conversation-archive pipeline. One bash invocation per distinct thread,
|
|
13
|
+
// one `:ConversationArchive {source:'email'}` per thread.
|
|
14
|
+
//
|
|
15
|
+
// Thread grouping uses the same In-Reply-To root derivation that the
|
|
16
|
+
// retired graph.ts writer used: a message whose parent hasn't arrived yet
|
|
17
|
+
// becomes its own thread root. Participant resolution is closed-set:
|
|
18
|
+
// every From/To/Cc address must already exist as `:Person.email` or
|
|
19
|
+
// `:AdminUser.email`. Any unresolved address skips its thread and returns
|
|
20
|
+
// in `unresolvedAddresses` for the caller to surface to the operator;
|
|
21
|
+
// :Person nodes are never auto-created here.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
function ingestBashPath() {
|
|
24
|
+
const platformRoot = process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../..");
|
|
25
|
+
return resolve(platformRoot, "plugins/memory/bin/conversation-archive-ingest.sh");
|
|
26
|
+
}
|
|
27
|
+
export async function dispatchEmailMessagesToConversationArchive(messages) {
|
|
28
|
+
if (messages.length === 0) {
|
|
29
|
+
return { threadsDispatched: 0, threadsSkipped: 0, unresolvedAddresses: [] };
|
|
30
|
+
}
|
|
31
|
+
const accountId = messages[0].accountId;
|
|
32
|
+
const threads = groupByThread(messages);
|
|
33
|
+
const distinctAddresses = new Set();
|
|
34
|
+
for (const t of threads)
|
|
35
|
+
for (const a of t.addresses)
|
|
36
|
+
distinctAddresses.add(a);
|
|
37
|
+
const addressToElementId = await resolveAddresses([...distinctAddresses], accountId);
|
|
38
|
+
let threadsDispatched = 0;
|
|
39
|
+
let threadsSkipped = 0;
|
|
40
|
+
const unresolved = new Set();
|
|
41
|
+
for (const thread of threads) {
|
|
42
|
+
const ids = [];
|
|
43
|
+
let threadOk = true;
|
|
44
|
+
for (const addr of thread.addresses) {
|
|
45
|
+
const elementId = addressToElementId.get(addr);
|
|
46
|
+
if (!elementId) {
|
|
47
|
+
unresolved.add(addr);
|
|
48
|
+
threadOk = false;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
ids.push(elementId);
|
|
52
|
+
}
|
|
53
|
+
if (!threadOk) {
|
|
54
|
+
threadsSkipped += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "email-thread-"));
|
|
58
|
+
const jsonPath = join(tmpDir, `thread-${thread.rootMessageId.replace(/[^a-z0-9-]+/gi, "_")}.json`);
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(jsonPath, JSON.stringify(buildThreadJson(thread)));
|
|
61
|
+
const exit = spawnSync("bash", [
|
|
62
|
+
ingestBashPath(),
|
|
63
|
+
jsonPath,
|
|
64
|
+
"--source",
|
|
65
|
+
"email",
|
|
66
|
+
"--participant-person-ids",
|
|
67
|
+
ids.join(","),
|
|
68
|
+
"--scope",
|
|
69
|
+
"admin",
|
|
70
|
+
"--session-id",
|
|
71
|
+
`email-${randomUUID()}`,
|
|
72
|
+
], { stdio: "inherit" });
|
|
73
|
+
if (exit.status === 0) {
|
|
74
|
+
threadsDispatched += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
threadsSkipped += 1;
|
|
78
|
+
console.error(`[email-ingest] conversation-archive-ingest.sh failed for thread root=${thread.rootMessageId} status=${exit.status}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
threadsDispatched,
|
|
87
|
+
threadsSkipped,
|
|
88
|
+
unresolvedAddresses: [...unresolved].sort(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Thread grouping
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
function groupByThread(messages) {
|
|
95
|
+
const byMessageId = new Map();
|
|
96
|
+
for (const m of messages) {
|
|
97
|
+
const id = stripAngleBrackets(m.messageId);
|
|
98
|
+
if (id)
|
|
99
|
+
byMessageId.set(id, m);
|
|
100
|
+
}
|
|
101
|
+
const rootFor = new Map();
|
|
102
|
+
function resolveRoot(messageId, seen) {
|
|
103
|
+
if (rootFor.has(messageId))
|
|
104
|
+
return rootFor.get(messageId);
|
|
105
|
+
if (seen.has(messageId))
|
|
106
|
+
return messageId; // cycle guard
|
|
107
|
+
seen.add(messageId);
|
|
108
|
+
const m = byMessageId.get(messageId);
|
|
109
|
+
const parent = stripAngleBrackets(m?.inReplyTo ?? null);
|
|
110
|
+
const root = parent && byMessageId.has(parent) ? resolveRoot(parent, seen) : messageId;
|
|
111
|
+
rootFor.set(messageId, root);
|
|
112
|
+
return root;
|
|
113
|
+
}
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const m of messages) {
|
|
116
|
+
const id = stripAngleBrackets(m.messageId);
|
|
117
|
+
const rootMessageId = id
|
|
118
|
+
? resolveRoot(id, new Set())
|
|
119
|
+
: `synthetic:${m.accountId}:${m.uidValidity}:${m.uid}`;
|
|
120
|
+
let g = groups.get(rootMessageId);
|
|
121
|
+
if (!g) {
|
|
122
|
+
g = { rootMessageId, messages: [], addresses: [] };
|
|
123
|
+
groups.set(rootMessageId, g);
|
|
124
|
+
}
|
|
125
|
+
g.messages.push(m);
|
|
126
|
+
}
|
|
127
|
+
for (const g of groups.values()) {
|
|
128
|
+
g.messages.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
|
|
129
|
+
const addrs = new Set();
|
|
130
|
+
for (const m of g.messages) {
|
|
131
|
+
if (m.fromAddress)
|
|
132
|
+
addrs.add(m.fromAddress.trim().toLowerCase());
|
|
133
|
+
for (const a of m.toAddresses)
|
|
134
|
+
addrs.add(a.trim().toLowerCase());
|
|
135
|
+
for (const a of m.ccAddresses)
|
|
136
|
+
addrs.add(a.trim().toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
g.addresses = [...addrs].filter((a) => a.length > 0).sort();
|
|
139
|
+
}
|
|
140
|
+
return [...groups.values()];
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Participant resolution
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
async function resolveAddresses(addresses, accountId) {
|
|
146
|
+
const map = new Map();
|
|
147
|
+
if (addresses.length === 0)
|
|
148
|
+
return map;
|
|
149
|
+
const session = getSession();
|
|
150
|
+
try {
|
|
151
|
+
const r = await session.run(`UNWIND $addresses AS addr
|
|
152
|
+
OPTIONAL MATCH (p) WHERE (p:Person OR p:AdminUser)
|
|
153
|
+
AND p.accountId = $accountId
|
|
154
|
+
AND toLower(p.email) = addr
|
|
155
|
+
WITH addr, p
|
|
156
|
+
WHERE p IS NOT NULL
|
|
157
|
+
RETURN addr, elementId(p) AS eid`, { addresses, accountId });
|
|
158
|
+
for (const rec of r.records) {
|
|
159
|
+
map.set(rec.get("addr"), rec.get("eid"));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await session.close();
|
|
164
|
+
}
|
|
165
|
+
return map;
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Thread JSON shape (matches conversation-normalisers/email.ts)
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
function buildThreadJson(thread) {
|
|
171
|
+
return thread.messages.map((m) => ({
|
|
172
|
+
from: { name: m.fromName ?? null, email: m.fromAddress },
|
|
173
|
+
date: m.receivedAt,
|
|
174
|
+
bodyText: m.bodyPreview,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=conversation-archive-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-archive-dispatch.js","sourceRoot":"","sources":["../../src/lib/conversation-archive-dispatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getSession } from "./neo4j.js";
|
|
2
|
+
import neo4j from "neo4j-driver";
|
|
1
3
|
export interface EmailData {
|
|
2
4
|
messageId: string | null;
|
|
3
5
|
inReplyTo: string | null;
|
|
@@ -22,13 +24,6 @@ export interface EmailData {
|
|
|
22
24
|
* "<abc@host>" → "abc@host". Already-clean values pass through unchanged.
|
|
23
25
|
*/
|
|
24
26
|
export declare function stripAngleBrackets(value: string | null): string | null;
|
|
25
|
-
/**
|
|
26
|
-
* Per-message envelope. Stored as a JSON-serialised array on the thread KD
|
|
27
|
-
* (`messages` property) so single-message lookup (email-reply, email-search
|
|
28
|
-
* detail) can answer "who sent THIS messageId" without parsing body text.
|
|
29
|
-
*
|
|
30
|
-
* Array order matches `messageIds`.
|
|
31
|
-
*/
|
|
32
27
|
export interface ThreadMessage {
|
|
33
28
|
messageId: string;
|
|
34
29
|
fromAddress: string;
|
|
@@ -40,56 +35,13 @@ export interface ThreadMessage {
|
|
|
40
35
|
uid: number;
|
|
41
36
|
mailbox: string;
|
|
42
37
|
}
|
|
43
|
-
|
|
44
|
-
attachmentId: string;
|
|
45
|
-
created: boolean;
|
|
46
|
-
appended: boolean;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Ingest one IMAP message into the per-thread :KnowledgeDocument.
|
|
50
|
-
*
|
|
51
|
-
* - If the message's In-Reply-To points to a known thread KD, the
|
|
52
|
-
* message is appended (body grows, messageIds grows, lastReceivedAt
|
|
53
|
-
* advances).
|
|
54
|
-
* - Otherwise, a new thread KD is created with this message as root.
|
|
55
|
-
*
|
|
56
|
-
* Person, address-edge, anchor, and embedding writes always run.
|
|
57
|
-
* Discarded-screening messages skip embedding to keep vector search clean.
|
|
58
|
-
*/
|
|
59
|
-
export declare function storeEmailMessage(data: EmailData): Promise<WriteResult>;
|
|
60
|
-
/**
|
|
61
|
-
* Ingest a batch of IMAP messages. Returns aggregate counts.
|
|
62
|
-
*
|
|
63
|
-
* Threads are intentionally not reordered — messages are processed in
|
|
64
|
-
* input order. Callers that want chronological transcript shape should
|
|
65
|
-
* pre-sort by `receivedAt` ascending.
|
|
66
|
-
*/
|
|
67
|
-
export declare function storeEmailMessages(messages: EmailData[]): Promise<{
|
|
68
|
-
threadsCreated: number;
|
|
69
|
-
messagesAppended: number;
|
|
70
|
-
duplicates: number;
|
|
71
|
-
}>;
|
|
72
|
-
/**
|
|
73
|
-
* Look up a single message envelope by its Message-ID. Walks the thread
|
|
74
|
-
* KD's `messages` array (JSON-encoded). Returns null when the thread KD
|
|
75
|
-
* has not been ingested yet.
|
|
76
|
-
*/
|
|
77
|
-
export declare function getMessageEnvelope(messageId: string, accountId: string): Promise<ThreadMessage | null>;
|
|
38
|
+
export declare function getMessageEnvelope(_messageId: string, _accountId: string): Promise<ThreadMessage | null>;
|
|
78
39
|
export interface ThreadInfo {
|
|
79
40
|
messageId: string;
|
|
80
41
|
threadDepth: number;
|
|
81
42
|
threadId: string;
|
|
82
43
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* - threadId — the thread KD's attachmentId
|
|
87
|
-
* - threadDepth — position of the message in the KD's messageIds array
|
|
88
|
-
* (0 = root). Useful for sorting search hits by thread
|
|
89
|
-
* position.
|
|
90
|
-
*
|
|
91
|
-
* Messages whose thread KD has not been ingested are absent from the map.
|
|
92
|
-
*/
|
|
93
|
-
export declare function getThreadInfo(messageIds: string[], accountId: string): Promise<Map<string, ThreadInfo>>;
|
|
94
|
-
export {};
|
|
44
|
+
export declare function getThreadInfo(_messageIds: string[], _accountId: string): Promise<Map<string, ThreadInfo>>;
|
|
45
|
+
export { neo4j };
|
|
46
|
+
export { getSession };
|
|
95
47
|
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;AAejC,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,CAAC;IAChE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAOtE;AAUD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAG/B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EAAE,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAIlC;AAKD,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { getSession } from "./neo4j.js";
|
|
3
|
-
import { computeEmbedding } from "./embedding.js";
|
|
4
2
|
import neo4j from "neo4j-driver";
|
|
5
3
|
/**
|
|
6
4
|
* Strip RFC 2822 angle brackets from a Message-ID or In-Reply-To header.
|
|
@@ -15,304 +13,18 @@ export function stripAngleBrackets(value) {
|
|
|
15
13
|
}
|
|
16
14
|
return trimmed;
|
|
17
15
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
.update(`email:${accountId}:${rootMessageId}`)
|
|
22
|
-
.digest("hex");
|
|
16
|
+
export async function getMessageEnvelope(_messageId, _accountId) {
|
|
17
|
+
// Task 442 — sourcing per-message envelopes from IMAP, not the graph.
|
|
18
|
+
return null;
|
|
23
19
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
}
|
|
46
|
-
/**
|
|
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).
|
|
51
|
-
*/
|
|
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();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
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.
|
|
86
|
-
*
|
|
87
|
-
* Person, address-edge, anchor, and embedding writes always run.
|
|
88
|
-
* Discarded-screening messages skip embedding to keep vector search clean.
|
|
89
|
-
*/
|
|
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 };
|
|
100
|
-
}
|
|
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.
|
|
127
|
-
let embedding = [];
|
|
128
|
-
if (data.screening !== "discarded") {
|
|
129
|
-
const embeddingText = [data.subject, data.bodyPreview].filter(Boolean).join(" — ");
|
|
130
|
-
embedding = await computeEmbedding(embeddingText);
|
|
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) }));
|
|
150
|
-
const session = getSession();
|
|
151
|
-
try {
|
|
152
|
-
await session.run(`MERGE (kd:KnowledgeDocument {attachmentId: $attachmentId})
|
|
153
|
-
ON CREATE SET
|
|
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,
|
|
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,
|
|
180
|
-
screening: data.screening ?? null,
|
|
181
|
-
screeningReason: data.screeningReason ?? null,
|
|
182
|
-
promptInjectionFlag: data.promptInjectionFlag ?? null,
|
|
183
|
-
});
|
|
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 });
|
|
209
|
-
}
|
|
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 });
|
|
215
|
-
}
|
|
216
|
-
return { attachmentId, created: !existing, appended: !!existing };
|
|
217
|
-
}
|
|
218
|
-
catch (err) {
|
|
219
|
-
if (err instanceof neo4j.Neo4jError &&
|
|
220
|
-
err.code === "Neo.ClientError.Schema.ConstraintValidationFailed") {
|
|
221
|
-
// Race: two concurrent writes for the same attachmentId. Treat as duplicate.
|
|
222
|
-
console.error(`[email-ingest] INFO: constraint caught duplicate attachmentId=${attachmentId} accountId=${data.accountId}`);
|
|
223
|
-
return { attachmentId, created: false, appended: false };
|
|
224
|
-
}
|
|
225
|
-
throw err;
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
await session.close();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
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.
|
|
237
|
-
*/
|
|
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);
|
|
244
|
-
if (result.created) {
|
|
245
|
-
threadsCreated++;
|
|
246
|
-
}
|
|
247
|
-
else if (result.appended) {
|
|
248
|
-
messagesAppended++;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
duplicates++;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return { threadsCreated, messagesAppended, duplicates };
|
|
255
|
-
}
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// Read helpers — used by email-reply, email-read, email-search to look
|
|
258
|
-
// individual messages and threads back out of the KD model.
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
/**
|
|
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.
|
|
264
|
-
*/
|
|
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;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
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.
|
|
282
|
-
*
|
|
283
|
-
* Messages whose thread KD has not been ingested are absent from the map.
|
|
284
|
-
*/
|
|
285
|
-
export async function getThreadInfo(messageIds, accountId) {
|
|
286
|
-
if (messageIds.length === 0)
|
|
287
|
-
return new Map();
|
|
288
|
-
const normalised = messageIds
|
|
289
|
-
.map((mid) => stripAngleBrackets(mid))
|
|
290
|
-
.filter((mid) => mid !== null);
|
|
291
|
-
if (normalised.length === 0)
|
|
292
|
-
return new Map();
|
|
293
|
-
const session = getSession();
|
|
294
|
-
try {
|
|
295
|
-
// Index-of-mid is computed inline with a list comprehension so the query
|
|
296
|
-
// does not depend on the apoc plugin being installed.
|
|
297
|
-
const result = await session.run(`UNWIND $messageIds AS mid
|
|
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
|
|
302
|
-
RETURN mid AS messageId, threadDepth, threadId`, { messageIds: normalised, accountId });
|
|
303
|
-
const map = new Map();
|
|
304
|
-
for (const record of result.records) {
|
|
305
|
-
const mid = record.get("messageId");
|
|
306
|
-
map.set(mid, {
|
|
307
|
-
messageId: mid,
|
|
308
|
-
threadDepth: neo4j.integer.toNumber(record.get("threadDepth")),
|
|
309
|
-
threadId: record.get("threadId"),
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
return map;
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
await session.close();
|
|
316
|
-
}
|
|
20
|
+
export async function getThreadInfo(_messageIds, _accountId) {
|
|
21
|
+
// Task 442 — per-message thread-depth lookup requires per-message nodes
|
|
22
|
+
// the new `:ConversationArchive` shape does not store.
|
|
23
|
+
return new Map();
|
|
317
24
|
}
|
|
25
|
+
// Re-export neo4j so consumers depending on the legacy module surface keep
|
|
26
|
+
// working without churn. The actual neo4j-driver usage is now in
|
|
27
|
+
// conversation-archive-dispatch.ts.
|
|
28
|
+
export { neo4j };
|
|
29
|
+
export { getSession };
|
|
318
30
|
//# sourceMappingURL=graph.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;AAmCjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAsBD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,UAAkB,EAClB,UAAkB;IAElB,sEAAsE;IACtE,OAAO,IAAI,CAAC;AACd,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,WAAqB,EACrB,UAAkB;IAElB,wEAAwE;IACxE,uDAAuD;IACvD,OAAO,IAAI,GAAG,EAAE,CAAC;AACnB,CAAC;AAED,2EAA2E;AAC3E,iEAAiE;AACjE,oCAAoC;AACpC,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-classify.d.ts","sourceRoot":"","sources":["../../src/tools/email-classify.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAuCD,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8DhF"}
|