@poncho-ai/messaging 0.7.10 → 0.8.0

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/messaging@0.7.10 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
2
+ > @poncho-ai/messaging@0.8.0 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 52.02 KB
11
- ESM ⚡️ Build success in 128ms
10
+ ESM dist/index.js 53.03 KB
11
+ ESM ⚡️ Build success in 66ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 4935ms
13
+ DTS ⚡️ Build success in 4862ms
14
14
  DTS dist/index.d.ts 11.66 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @poncho-ai/messaging
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`fb7ee97`](https://github.com/cesr/poncho-ai/commit/fb7ee97f7df0dda7318a7e59565e0b53285f10c4) Thanks [@cesr](https://github.com/cesr)! - feat: include thread context when Slack bot is @mentioned in a thread reply
8
+
9
+ When the bot is @mentioned in a thread (not the parent message), the adapter now fetches prior thread messages via `conversations.replies` and prepends them as context, so the agent understands what the conversation is about.
10
+
3
11
  ## 0.7.10
4
12
 
5
13
  ### Patch Changes
package/dist/index.js CHANGED
@@ -203,6 +203,21 @@ var addReaction = async (token, channel, timestamp, reaction) => {
203
203
  throw new Error(`Slack reactions.add failed: ${result.error}`);
204
204
  }
205
205
  };
206
+ var fetchThreadMessages = async (token, channel, threadTs, excludeTs) => {
207
+ const result = await slackFetch("conversations.replies", token, {
208
+ channel,
209
+ ts: threadTs,
210
+ limit: 50
211
+ });
212
+ if (!result.ok) {
213
+ console.warn(
214
+ `[slack-adapter] conversations.replies failed: ${result.error}`
215
+ );
216
+ return [];
217
+ }
218
+ const messages = result.messages ?? [];
219
+ return excludeTs ? messages.filter((m) => m.ts !== excludeTs) : messages;
220
+ };
206
221
  var removeReaction = async (token, channel, timestamp, reaction) => {
207
222
  const result = await slackFetch("reactions.remove", token, {
208
223
  channel,
@@ -335,8 +350,26 @@ var SlackAdapter = class {
335
350
  const messageTs = String(event.ts ?? "");
336
351
  const channel = String(event.channel ?? "");
337
352
  const userId = String(event.user ?? "");
353
+ let contextPrefix = "";
354
+ if (event.thread_ts && event.thread_ts !== event.ts) {
355
+ const threadMessages = await fetchThreadMessages(
356
+ this.botToken,
357
+ channel,
358
+ threadTs,
359
+ messageTs
360
+ // exclude the triggering message itself
361
+ );
362
+ if (threadMessages.length > 0) {
363
+ const formatted = threadMessages.map((m) => `<${m.user ?? "unknown"}>: ${m.text ?? ""}`).join("\n");
364
+ contextPrefix = `[Thread context]
365
+ ${formatted}
366
+
367
+ [New message]
368
+ `;
369
+ }
370
+ }
338
371
  const message = {
339
- text,
372
+ text: contextPrefix + text,
340
373
  threadRef: {
341
374
  platformThreadId: threadTs,
342
375
  channelId: channel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/messaging",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,6 +9,7 @@ import type {
9
9
  import { verifySlackSignature } from "./verify.js";
10
10
  import {
11
11
  addReaction,
12
+ fetchThreadMessages,
12
13
  postMessage,
13
14
  removeReaction,
14
15
  splitMessage,
@@ -189,8 +190,27 @@ export class SlackAdapter implements MessagingAdapter {
189
190
  const channel = String(event.channel ?? "");
190
191
  const userId = String(event.user ?? "");
191
192
 
193
+ // When the mention is inside a thread (thread_ts differs from ts),
194
+ // fetch prior thread messages so the agent has context.
195
+ let contextPrefix = "";
196
+ if (event.thread_ts && event.thread_ts !== event.ts) {
197
+ const threadMessages = await fetchThreadMessages(
198
+ this.botToken,
199
+ channel,
200
+ threadTs,
201
+ messageTs, // exclude the triggering message itself
202
+ );
203
+
204
+ if (threadMessages.length > 0) {
205
+ const formatted = threadMessages
206
+ .map((m) => `<${m.user ?? "unknown"}>: ${m.text ?? ""}`)
207
+ .join("\n");
208
+ contextPrefix = `[Thread context]\n${formatted}\n\n[New message]\n`;
209
+ }
210
+ }
211
+
192
212
  const message: PonchoIncomingMessage = {
193
- text,
213
+ text: contextPrefix + text,
194
214
  threadRef: {
195
215
  platformThreadId: threadTs,
196
216
  channelId: channel,
@@ -90,6 +90,38 @@ export const addReaction = async (
90
90
  }
91
91
  };
92
92
 
93
+ /**
94
+ * Fetch thread replies using `conversations.replies`.
95
+ * Returns messages sorted chronologically (oldest first), excluding the
96
+ * message identified by `excludeTs` (typically the triggering mention).
97
+ */
98
+ export const fetchThreadMessages = async (
99
+ token: string,
100
+ channel: string,
101
+ threadTs: string,
102
+ excludeTs?: string,
103
+ ): Promise<Array<{ user?: string; text?: string; ts: string }>> => {
104
+ const result = (await slackFetch("conversations.replies", token, {
105
+ channel,
106
+ ts: threadTs,
107
+ limit: 50,
108
+ })) as {
109
+ ok: boolean;
110
+ error?: string;
111
+ messages?: Array<{ user?: string; text?: string; ts: string }>;
112
+ };
113
+
114
+ if (!result.ok) {
115
+ console.warn(
116
+ `[slack-adapter] conversations.replies failed: ${result.error}`,
117
+ );
118
+ return [];
119
+ }
120
+
121
+ const messages = result.messages ?? [];
122
+ return excludeTs ? messages.filter((m) => m.ts !== excludeTs) : messages;
123
+ };
124
+
93
125
  export const removeReaction = async (
94
126
  token: string,
95
127
  channel: string,
@@ -238,4 +238,96 @@ describe("SlackAdapter", () => {
238
238
  expect(received).toHaveLength(0);
239
239
  expect(resStatus()).toBe(200);
240
240
  });
241
+
242
+ it("includes thread context when mention is in a thread reply", async () => {
243
+ const adapter = new SlackAdapter();
244
+ await adapter.initialize();
245
+ const received: PonchoIncomingMessage[] = [];
246
+ adapter.onMessage(async (msg) => { received.push(msg); });
247
+
248
+ // Mock fetch to intercept conversations.replies
249
+ const originalFetch = globalThis.fetch;
250
+ vi.stubGlobal("fetch", vi.fn(async (url: string) => {
251
+ if (typeof url === "string" && url.includes("conversations.replies")) {
252
+ return {
253
+ json: async () => ({
254
+ ok: true,
255
+ messages: [
256
+ { user: "U_PARENT", text: "What are the limitations of self-hosted?", ts: "100.0" },
257
+ { user: "U2", text: "<@U1> can you help?", ts: "100.1" },
258
+ ],
259
+ }),
260
+ };
261
+ }
262
+ // Pass through for other Slack API calls (reactions)
263
+ return { json: async () => ({ ok: true }) };
264
+ }));
265
+
266
+ let handler: any;
267
+ adapter.registerRoutes((_m, _p, h) => { handler = h; });
268
+
269
+ const body = JSON.stringify({
270
+ type: "event_callback",
271
+ event: {
272
+ type: "app_mention",
273
+ text: "<@U1> can you help?",
274
+ ts: "100.1",
275
+ thread_ts: "100.0",
276
+ channel: "C1",
277
+ user: "U2",
278
+ },
279
+ });
280
+ const { req, res } = makeReqRes(body);
281
+ await handler(req, res);
282
+
283
+ // Wait for async handler
284
+ await new Promise((r) => setTimeout(r, 100));
285
+
286
+ expect(received).toHaveLength(1);
287
+ expect(received[0]!.text).toContain("[Thread context]");
288
+ expect(received[0]!.text).toContain("What are the limitations of self-hosted?");
289
+ expect(received[0]!.text).toContain("[New message]");
290
+ expect(received[0]!.text).toContain("can you help?");
291
+
292
+ vi.stubGlobal("fetch", originalFetch);
293
+ });
294
+
295
+ it("does not fetch thread context for top-level mentions", async () => {
296
+ const adapter = new SlackAdapter();
297
+ await adapter.initialize();
298
+ const received: PonchoIncomingMessage[] = [];
299
+ adapter.onMessage(async (msg) => { received.push(msg); });
300
+
301
+ const fetchSpy = vi.fn(async () => ({ json: async () => ({ ok: true }) }));
302
+ vi.stubGlobal("fetch", fetchSpy);
303
+
304
+ let handler: any;
305
+ adapter.registerRoutes((_m, _p, h) => { handler = h; });
306
+
307
+ // No thread_ts → top-level message
308
+ const body = JSON.stringify({
309
+ type: "event_callback",
310
+ event: {
311
+ type: "app_mention",
312
+ text: "<@U1> hello",
313
+ ts: "200.0",
314
+ channel: "C1",
315
+ user: "U2",
316
+ },
317
+ });
318
+ const { req, res } = makeReqRes(body);
319
+ await handler(req, res);
320
+
321
+ await new Promise((r) => setTimeout(r, 100));
322
+
323
+ expect(received).toHaveLength(1);
324
+ expect(received[0]!.text).toBe("hello");
325
+ // conversations.replies should NOT have been called
326
+ const repliesCalls = fetchSpy.mock.calls.filter(
327
+ ([url]: any) => typeof url === "string" && url.includes("conversations.replies"),
328
+ );
329
+ expect(repliesCalls).toHaveLength(0);
330
+
331
+ vi.restoreAllMocks();
332
+ });
241
333
  });