@poncho-ai/messaging 0.7.6 → 0.7.8

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.6 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
2
+ > @poncho-ai/messaging@0.7.8 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 51.09 KB
11
- ESM ⚡️ Build success in 102ms
10
+ ESM dist/index.js 51.91 KB
11
+ ESM ⚡️ Build success in 49ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 4825ms
14
- DTS dist/index.d.ts 11.24 KB
13
+ DTS ⚡️ Build success in 5402ms
14
+ DTS dist/index.d.ts 11.66 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @poncho-ai/messaging
2
2
 
3
+ ## 0.7.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [`30026c5`](https://github.com/cesr/poncho-ai/commit/30026c5eba3f714bb80c2402c5e8f32c6fd38d87) Thanks [@cesr](https://github.com/cesr)! - Fix Telegram conversation instability on serverless: use stable platformThreadId instead of in-memory session counter.
8
+
9
+ ## 0.7.7
10
+
11
+ ### Patch Changes
12
+
13
+ - [#61](https://github.com/cesr/poncho-ai/pull/61) [`0a51abe`](https://github.com/cesr/poncho-ai/commit/0a51abec12191397fd36ab1fd4feca7460489e33) Thanks [@cesr](https://github.com/cesr)! - Fix /new command on Telegram in serverless environments: persist conversation reset to the store so it survives cold starts.
14
+
3
15
  ## 0.7.6
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ interface IncomingMessage {
26
26
  raw: unknown;
27
27
  }
28
28
  type IncomingMessageHandler = (message: IncomingMessage) => Promise<void>;
29
+ type ResetHandler = (platform: string, threadRef: ThreadRef) => Promise<void>;
29
30
  type RouteHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
30
31
  type RouteRegistrar = (method: "GET" | "POST", path: string, handler: RouteHandler) => void;
31
32
  interface MessagingAdapter {
@@ -43,6 +44,8 @@ interface MessagingAdapter {
43
44
  initialize(): Promise<void>;
44
45
  /** Set the handler that processes incoming messages. */
45
46
  onMessage(handler: IncomingMessageHandler): void;
47
+ /** Set the handler called when a user resets the conversation (e.g. /new). */
48
+ onReset?(handler: ResetHandler): void;
46
49
  /** Post a reply back to the originating thread. */
47
50
  sendReply(threadRef: ThreadRef, content: string, options?: {
48
51
  files?: FileAttachment[];
@@ -89,6 +92,11 @@ interface AgentRunner {
89
92
  steps?: number;
90
93
  maxSteps?: number;
91
94
  }>;
95
+ /**
96
+ * Reset a conversation by clearing its messages, making the next
97
+ * interaction start fresh while keeping the same conversation ID.
98
+ */
99
+ resetConversation?(conversationId: string): Promise<void>;
92
100
  }
93
101
  interface AgentBridgeOptions {
94
102
  adapter: MessagingAdapter;
@@ -223,13 +231,14 @@ declare class TelegramAdapter implements MessagingAdapter {
223
231
  private readonly webhookSecretEnv;
224
232
  private readonly allowedUserIds;
225
233
  private handler;
234
+ private resetHandler;
226
235
  private approvalDecisionHandler;
227
- private readonly sessionCounters;
228
236
  private readonly approvalMessageIds;
229
237
  private lastUpdateId;
230
238
  constructor(options?: TelegramAdapterOptions);
231
239
  initialize(): Promise<void>;
232
240
  onMessage(handler: IncomingMessageHandler): void;
241
+ onReset(handler: ResetHandler): void;
233
242
  registerRoutes(router: RouteRegistrar): void;
234
243
  sendReply(threadRef: ThreadRef, content: string, options?: {
235
244
  files?: FileAttachment[];
@@ -242,7 +251,6 @@ declare class TelegramAdapter implements MessagingAdapter {
242
251
  updateApprovalMessage(approvalId: string, decision: "approved" | "denied", tool: string): Promise<void>;
243
252
  private handleRequest;
244
253
  private handleCallbackQuery;
245
- private sessionKey;
246
254
  private extractFiles;
247
255
  }
248
256
 
package/dist/index.js CHANGED
@@ -30,6 +30,12 @@ var AgentBridge = class {
30
30
  this.waitUntil(processing);
31
31
  return processing;
32
32
  });
33
+ if (this.adapter.onReset && this.runner.resetConversation) {
34
+ this.adapter.onReset(async (platform, threadRef) => {
35
+ const conversationId = conversationIdFromThread(platform, threadRef);
36
+ await this.runner.resetConversation(conversationId);
37
+ });
38
+ }
33
39
  await this.adapter.initialize();
34
40
  }
35
41
  async handleMessage(message) {
@@ -1186,6 +1192,12 @@ var stripMention2 = (text, entities, botUsername, botId) => {
1186
1192
  // src/adapters/telegram/index.ts
1187
1193
  var TYPING_INTERVAL_MS = 4e3;
1188
1194
  var NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
1195
+ var parseMessageThreadId = (platformThreadId, chatId) => {
1196
+ const parts = platformThreadId.split(":");
1197
+ if (parts.length !== 3 || parts[0] !== chatId) return void 0;
1198
+ const threadId = Number(parts[1]);
1199
+ return Number.isInteger(threadId) ? threadId : void 0;
1200
+ };
1189
1201
  var collectBody3 = (req) => new Promise((resolve, reject) => {
1190
1202
  const chunks = [];
1191
1203
  req.on("data", (chunk) => chunks.push(chunk));
@@ -1204,8 +1216,8 @@ var TelegramAdapter = class {
1204
1216
  webhookSecretEnv;
1205
1217
  allowedUserIds;
1206
1218
  handler;
1219
+ resetHandler;
1207
1220
  approvalDecisionHandler;
1208
- sessionCounters = /* @__PURE__ */ new Map();
1209
1221
  approvalMessageIds = /* @__PURE__ */ new Map();
1210
1222
  lastUpdateId = 0;
1211
1223
  constructor(options = {}) {
@@ -1231,6 +1243,9 @@ var TelegramAdapter = class {
1231
1243
  onMessage(handler) {
1232
1244
  this.handler = handler;
1233
1245
  }
1246
+ onReset(handler) {
1247
+ this.resetHandler = handler;
1248
+ }
1234
1249
  registerRoutes(router) {
1235
1250
  router(
1236
1251
  "POST",
@@ -1241,11 +1256,16 @@ var TelegramAdapter = class {
1241
1256
  async sendReply(threadRef, content, options) {
1242
1257
  const chatId = threadRef.channelId;
1243
1258
  const replyTo = threadRef.messageId ? Number(threadRef.messageId) : void 0;
1259
+ const messageThreadId = parseMessageThreadId(
1260
+ threadRef.platformThreadId,
1261
+ chatId
1262
+ );
1244
1263
  if (content) {
1245
1264
  const chunks = splitMessage2(content);
1246
1265
  for (const chunk of chunks) {
1247
1266
  await sendMessage(this.botToken, chatId, chunk, {
1248
- reply_to_message_id: replyTo
1267
+ reply_to_message_id: replyTo,
1268
+ message_thread_id: messageThreadId
1249
1269
  });
1250
1270
  }
1251
1271
  }
@@ -1254,11 +1274,13 @@ var TelegramAdapter = class {
1254
1274
  if (file.mediaType.startsWith("image/")) {
1255
1275
  await sendPhoto(this.botToken, chatId, file.data, {
1256
1276
  reply_to_message_id: replyTo,
1277
+ message_thread_id: messageThreadId,
1257
1278
  filename: file.filename
1258
1279
  });
1259
1280
  } else {
1260
1281
  await sendDocument(this.botToken, chatId, file.data, {
1261
1282
  reply_to_message_id: replyTo,
1283
+ message_thread_id: messageThreadId,
1262
1284
  filename: file.filename,
1263
1285
  mediaType: file.mediaType
1264
1286
  });
@@ -1413,11 +1435,20 @@ ${inputSummary}` : "(no input)"
1413
1435
  res.end();
1414
1436
  return;
1415
1437
  }
1416
- const key2 = this.sessionKey(message);
1417
- const current = this.sessionCounters.get(key2) ?? 0;
1418
- this.sessionCounters.set(key2, current + 1);
1419
1438
  res.writeHead(200);
1420
1439
  res.end();
1440
+ if (this.resetHandler) {
1441
+ const topicId2 = message.message_thread_id;
1442
+ const threadId = topicId2 ? `${chatId}:${topicId2}:0` : `${chatId}:0`;
1443
+ try {
1444
+ await this.resetHandler("telegram", {
1445
+ channelId: chatId,
1446
+ platformThreadId: threadId
1447
+ });
1448
+ } catch (err) {
1449
+ console.error("[telegram-adapter] reset handler error:", err instanceof Error ? err.message : err);
1450
+ }
1451
+ }
1421
1452
  await sendMessage(
1422
1453
  this.botToken,
1423
1454
  chatId,
@@ -1446,10 +1477,8 @@ ${inputSummary}` : "(no input)"
1446
1477
  res.end();
1447
1478
  if (!this.handler) return;
1448
1479
  const files = await this.extractFiles(message);
1449
- const key = this.sessionKey(message);
1450
- const session = this.sessionCounters.get(key) ?? 0;
1451
1480
  const topicId = message.message_thread_id;
1452
- const platformThreadId = topicId ? `${chatId}:${topicId}:${session}` : `${chatId}:${session}`;
1481
+ const platformThreadId = topicId ? `${chatId}:${topicId}:0` : `${chatId}:0`;
1453
1482
  const userId = String(message.from?.id ?? "unknown");
1454
1483
  const userName = [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ") || void 0;
1455
1484
  const ponchoMessage = {
@@ -1505,10 +1534,6 @@ ${inputSummary}` : "(no input)"
1505
1534
  // -----------------------------------------------------------------------
1506
1535
  // Helpers
1507
1536
  // -----------------------------------------------------------------------
1508
- sessionKey(message) {
1509
- const chatId = String(message.chat.id);
1510
- return message.message_thread_id ? `${chatId}:${message.message_thread_id}` : chatId;
1511
- }
1512
1537
  async extractFiles(message) {
1513
1538
  const files = [];
1514
1539
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/messaging",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,7 @@ import type {
4
4
  IncomingMessage as PonchoIncomingMessage,
5
5
  IncomingMessageHandler,
6
6
  MessagingAdapter,
7
+ ResetHandler,
7
8
  RouteRegistrar,
8
9
  ThreadRef,
9
10
  } from "../../types.js";
@@ -30,6 +31,19 @@ import {
30
31
  const TYPING_INTERVAL_MS = 4_000;
31
32
  const NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
32
33
 
34
+ const parseMessageThreadId = (
35
+ platformThreadId: string,
36
+ chatId: string,
37
+ ): number | undefined => {
38
+ const parts = platformThreadId.split(":");
39
+ // Telegram thread format:
40
+ // - non-topic chats: `${chatId}:${session}`
41
+ // - forum topics: `${chatId}:${message_thread_id}:${session}`
42
+ if (parts.length !== 3 || parts[0] !== chatId) return undefined;
43
+ const threadId = Number(parts[1]);
44
+ return Number.isInteger(threadId) ? threadId : undefined;
45
+ };
46
+
33
47
  export interface TelegramApprovalInfo {
34
48
  approvalId: string;
35
49
  tool: string;
@@ -69,8 +83,8 @@ export class TelegramAdapter implements MessagingAdapter {
69
83
  private readonly webhookSecretEnv: string;
70
84
  private readonly allowedUserIds: number[] | undefined;
71
85
  private handler: IncomingMessageHandler | undefined;
86
+ private resetHandler: ResetHandler | undefined;
72
87
  private approvalDecisionHandler: TelegramApprovalDecisionHandler | undefined;
73
- private readonly sessionCounters = new Map<string, number>();
74
88
  private readonly approvalMessageIds = new Map<string, { chatId: string; messageId: number }>();
75
89
  private lastUpdateId = 0;
76
90
 
@@ -107,6 +121,10 @@ export class TelegramAdapter implements MessagingAdapter {
107
121
  this.handler = handler;
108
122
  }
109
123
 
124
+ onReset(handler: ResetHandler): void {
125
+ this.resetHandler = handler;
126
+ }
127
+
110
128
  registerRoutes(router: RouteRegistrar): void {
111
129
  router("POST", "/api/messaging/telegram", (req, res) =>
112
130
  this.handleRequest(req, res),
@@ -122,12 +140,17 @@ export class TelegramAdapter implements MessagingAdapter {
122
140
  const replyTo = threadRef.messageId
123
141
  ? Number(threadRef.messageId)
124
142
  : undefined;
143
+ const messageThreadId = parseMessageThreadId(
144
+ threadRef.platformThreadId,
145
+ chatId,
146
+ );
125
147
 
126
148
  if (content) {
127
149
  const chunks = splitMessage(content);
128
150
  for (const chunk of chunks) {
129
151
  await sendMessage(this.botToken, chatId, chunk, {
130
152
  reply_to_message_id: replyTo,
153
+ message_thread_id: messageThreadId,
131
154
  });
132
155
  }
133
156
  }
@@ -137,11 +160,13 @@ export class TelegramAdapter implements MessagingAdapter {
137
160
  if (file.mediaType.startsWith("image/")) {
138
161
  await sendPhoto(this.botToken, chatId, file.data, {
139
162
  reply_to_message_id: replyTo,
163
+ message_thread_id: messageThreadId,
140
164
  filename: file.filename,
141
165
  });
142
166
  } else {
143
167
  await sendDocument(this.botToken, chatId, file.data, {
144
168
  reply_to_message_id: replyTo,
169
+ message_thread_id: messageThreadId,
145
170
  filename: file.filename,
146
171
  mediaType: file.mediaType,
147
172
  });
@@ -348,13 +373,25 @@ export class TelegramAdapter implements MessagingAdapter {
348
373
  return;
349
374
  }
350
375
 
351
- const key = this.sessionKey(message);
352
- const current = this.sessionCounters.get(key) ?? 0;
353
- this.sessionCounters.set(key, current + 1);
354
-
355
376
  res.writeHead(200);
356
377
  res.end();
357
378
 
379
+ // Clear conversation in the store so the next message starts fresh.
380
+ if (this.resetHandler) {
381
+ const topicId = message.message_thread_id;
382
+ const threadId = topicId
383
+ ? `${chatId}:${topicId}:0`
384
+ : `${chatId}:0`;
385
+ try {
386
+ await this.resetHandler("telegram", {
387
+ channelId: chatId,
388
+ platformThreadId: threadId,
389
+ });
390
+ } catch (err) {
391
+ console.error("[telegram-adapter] reset handler error:", err instanceof Error ? err.message : err);
392
+ }
393
+ }
394
+
358
395
  await sendMessage(
359
396
  this.botToken,
360
397
  chatId,
@@ -396,12 +433,12 @@ export class TelegramAdapter implements MessagingAdapter {
396
433
  const files = await this.extractFiles(message);
397
434
 
398
435
  // -- Build thread ref -------------------------------------------------
399
- const key = this.sessionKey(message);
400
- const session = this.sessionCounters.get(key) ?? 0;
436
+ // Always use a fixed session component so the conversationId is stable
437
+ // across serverless cold starts. /new resets via the store instead.
401
438
  const topicId = message.message_thread_id;
402
439
  const platformThreadId = topicId
403
- ? `${chatId}:${topicId}:${session}`
404
- : `${chatId}:${session}`;
440
+ ? `${chatId}:${topicId}:0`
441
+ : `${chatId}:0`;
405
442
 
406
443
  const userId = String(message.from?.id ?? "unknown");
407
444
  const userName =
@@ -476,13 +513,6 @@ export class TelegramAdapter implements MessagingAdapter {
476
513
  // Helpers
477
514
  // -----------------------------------------------------------------------
478
515
 
479
- private sessionKey(message: TelegramMessage): string {
480
- const chatId = String(message.chat.id);
481
- return message.message_thread_id
482
- ? `${chatId}:${message.message_thread_id}`
483
- : chatId;
484
- }
485
-
486
516
  private async extractFiles(
487
517
  message: TelegramMessage,
488
518
  ): Promise<FileAttachment[]> {
package/src/bridge.ts CHANGED
@@ -50,6 +50,14 @@ export class AgentBridge {
50
50
  this.waitUntil(processing);
51
51
  return processing;
52
52
  });
53
+
54
+ if (this.adapter.onReset && this.runner.resetConversation) {
55
+ this.adapter.onReset(async (platform, threadRef) => {
56
+ const conversationId = conversationIdFromThread(platform, threadRef);
57
+ await this.runner.resetConversation!(conversationId);
58
+ });
59
+ }
60
+
53
61
  await this.adapter.initialize();
54
62
  }
55
63
 
package/src/types.ts CHANGED
@@ -33,6 +33,11 @@ export type IncomingMessageHandler = (
33
33
  message: IncomingMessage,
34
34
  ) => Promise<void>;
35
35
 
36
+ export type ResetHandler = (
37
+ platform: string,
38
+ threadRef: ThreadRef,
39
+ ) => Promise<void>;
40
+
36
41
  // ---------------------------------------------------------------------------
37
42
  // Route registration (adapter ↔ HTTP server contract)
38
43
  // ---------------------------------------------------------------------------
@@ -73,6 +78,9 @@ export interface MessagingAdapter {
73
78
  /** Set the handler that processes incoming messages. */
74
79
  onMessage(handler: IncomingMessageHandler): void;
75
80
 
81
+ /** Set the handler called when a user resets the conversation (e.g. /new). */
82
+ onReset?(handler: ResetHandler): void;
83
+
76
84
  /** Post a reply back to the originating thread. */
77
85
  sendReply(
78
86
  threadRef: ThreadRef,
@@ -133,6 +141,12 @@ export interface AgentRunner {
133
141
  steps?: number;
134
142
  maxSteps?: number;
135
143
  }>;
144
+
145
+ /**
146
+ * Reset a conversation by clearing its messages, making the next
147
+ * interaction start fresh while keeping the same conversation ID.
148
+ */
149
+ resetConversation?(conversationId: string): Promise<void>;
136
150
  }
137
151
 
138
152
  // ---------------------------------------------------------------------------