MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / groupIntoThreads

Function groupIntoThreads

web/src/app/components/messages/threading.ts:97–169  ·  view source on GitHub ↗
(
  rows: MessageSummary[],
  now: Date = new Date(),
)

Source from the content-addressed store, hash-verified

95 * @param now Reference timestamp for "closed >7d" — caller injects so tests can pin it.
96 */
97export function groupIntoThreads(
98 rows: MessageSummary[],
99 now: Date = new Date(),
100): Thread[] {
101 const nowMs = now.getTime();
102 const buckets = new Map<string, MessageSummary[]>();
103
104 for (const m of rows) {
105 const key = m.conversation_id
106 ? `conv:${m.conversation_id}`
107 : `orphan:${m.message_id}`;
108 const bucket = buckets.get(key);
109 if (bucket) bucket.push(m);
110 else buckets.set(key, [m]);
111 }
112
113 const threads: Thread[] = [];
114 for (const [key, bucket] of buckets) {
115 // Order oldest → newest. Stable on created_at; ties broken by message_id
116 // so the order is deterministic even at sub-second resolution.
117 bucket.sort((a, b) => {
118 const ad = new Date(a.created_at).getTime();
119 const bd = new Date(b.created_at).getTime();
120 if (ad !== bd) return ad - bd;
121 return a.message_id.localeCompare(b.message_id);
122 });
123
124 const latest = bucket[bucket.length - 1];
125 const oldest = bucket[0];
126
127 // Subject: first non-empty in the thread, else "(no subject)".
128 const subject =
129 bucket.map((m) => m.subject).find((s) => s && s.trim() !== "") ||
130 "(no subject)";
131
132 // Counterparty: derived from any non-agent participant. We use the
133 // latest message's "other side" — typically what the user thinks of
134 // as "who this thread is with".
135 const cpEmail = counterpartyEmail(latest);
136 const counterparty: Counterparty = {
137 email: cpEmail,
138 name: nameFromEmail(cpEmail),
139 };
140
141 threads.push({
142 key,
143 conversationId: latest.conversation_id || undefined,
144 counterparty,
145 subject,
146 state: deriveState(bucket, nowMs),
147 lastMessageAt: latest.created_at,
148 startedAt: oldest.created_at,
149 msgCount: bucket.length,
150 lastDirection: latest.direction,
151 // Subject as preview for v1 — body parts aren't in the wire payload
152 // yet. When a server-side preview field lands, switch to that.
153 lastPreview: (latest.subject || "(no subject)").slice(0, 80),
154 messages: bucket,

Callers 2

threading.test.tsFile · 0.90
AgentInboxContentFunction · 0.90

Calls 5

counterpartyEmailFunction · 0.85
nameFromEmailFunction · 0.85
deriveStateFunction · 0.85
setMethod · 0.80
getMethod · 0.45

Tested by

no test coverage detected