chat 4.18.0 → 4.20.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.
Files changed (44) hide show
  1. package/README.md +2 -0
  2. package/dist/{chunk-WAB7KMH4.js → chunk-JW7GYSMH.js} +3 -3
  3. package/dist/chunk-JW7GYSMH.js.map +1 -0
  4. package/dist/index.d.ts +220 -13
  5. package/dist/index.js +274 -56
  6. package/dist/index.js.map +1 -1
  7. package/dist/jsx-runtime.d.ts +1 -1
  8. package/dist/jsx-runtime.js +1 -1
  9. package/docs/adapters/whatsapp.mdx +222 -0
  10. package/docs/{adapters/index.mdx → adapters.mdx} +46 -65
  11. package/docs/api/channel.mdx +15 -0
  12. package/docs/api/index.mdx +1 -0
  13. package/docs/api/thread.mdx +50 -0
  14. package/docs/contributing/building.mdx +1 -0
  15. package/docs/error-handling.mdx +2 -2
  16. package/docs/getting-started.mdx +2 -0
  17. package/docs/guides/code-review-hono.mdx +3 -3
  18. package/docs/guides/discord-nuxt.mdx +3 -2
  19. package/docs/guides/durable-chat-sessions-nextjs.mdx +331 -0
  20. package/docs/guides/meta.json +7 -1
  21. package/docs/guides/scheduled-posts-neon.mdx +447 -0
  22. package/docs/guides/slack-nextjs.mdx +3 -2
  23. package/docs/index.mdx +3 -1
  24. package/docs/meta.json +3 -4
  25. package/docs/slash-commands.mdx +4 -4
  26. package/docs/{state/index.mdx → state.mdx} +24 -12
  27. package/docs/threads-messages-channels.mdx +17 -0
  28. package/docs/usage.mdx +5 -0
  29. package/package.json +1 -1
  30. package/dist/chunk-WAB7KMH4.js.map +0 -1
  31. package/docs/adapters/discord.mdx +0 -217
  32. package/docs/adapters/gchat.mdx +0 -237
  33. package/docs/adapters/github.mdx +0 -222
  34. package/docs/adapters/linear.mdx +0 -206
  35. package/docs/adapters/meta.json +0 -13
  36. package/docs/adapters/slack.mdx +0 -314
  37. package/docs/adapters/teams.mdx +0 -287
  38. package/docs/adapters/telegram.mdx +0 -161
  39. package/docs/state/ioredis.mdx +0 -81
  40. package/docs/state/memory.mdx +0 -52
  41. package/docs/state/meta.json +0 -4
  42. package/docs/state/postgres.mdx +0 -98
  43. package/docs/state/redis.mdx +0 -100
  44. package/dist/{jsx-runtime-BYavlUk9.d.ts → jsx-runtime-C2ATKxHQ.d.ts} +95 -95
package/dist/index.js CHANGED
@@ -59,7 +59,20 @@ import {
59
59
  toModalElement,
60
60
  toPlainText,
61
61
  walkAst
62
- } from "./chunk-WAB7KMH4.js";
62
+ } from "./chunk-JW7GYSMH.js";
63
+
64
+ // src/ai.ts
65
+ function toAiMessages(messages, options) {
66
+ const includeNames = options?.includeNames ?? false;
67
+ const sorted = [...messages].sort(
68
+ (a, b) => (a.metadata.dateSent?.getTime() ?? 0) - (b.metadata.dateSent?.getTime() ?? 0)
69
+ );
70
+ return sorted.filter((msg) => msg.text.trim()).map((msg) => {
71
+ const role = msg.author.isMe ? "assistant" : "user";
72
+ const content = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
73
+ return { role, content };
74
+ });
75
+ }
63
76
 
64
77
  // src/channel.ts
65
78
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
@@ -81,6 +94,42 @@ function hasChatSingleton() {
81
94
  return _singleton !== null;
82
95
  }
83
96
 
97
+ // src/from-full-stream.ts
98
+ var STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
99
+ "markdown_text",
100
+ "task_update",
101
+ "plan_update"
102
+ ]);
103
+ async function* fromFullStream(stream) {
104
+ let needsSeparator = false;
105
+ let hasEmittedText = false;
106
+ for await (const event of stream) {
107
+ if (typeof event === "string") {
108
+ yield event;
109
+ continue;
110
+ }
111
+ if (event === null || typeof event !== "object" || !("type" in event)) {
112
+ continue;
113
+ }
114
+ const typed = event;
115
+ if (STREAM_CHUNK_TYPES.has(typed.type)) {
116
+ yield event;
117
+ continue;
118
+ }
119
+ const textContent = typed.text ?? typed.delta ?? typed.textDelta;
120
+ if (typed.type === "text-delta" && typeof textContent === "string") {
121
+ if (needsSeparator && hasEmittedText) {
122
+ yield "\n\n";
123
+ }
124
+ needsSeparator = false;
125
+ hasEmittedText = true;
126
+ yield textContent;
127
+ } else if (typed.type === "step-finish") {
128
+ needsSeparator = true;
129
+ }
130
+ }
131
+ }
132
+
84
133
  // src/message.ts
85
134
  import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
86
135
  var Message = class _Message {
@@ -301,6 +350,7 @@ var ChannelImpl = class _ChannelImpl {
301
350
  _adapterName;
302
351
  _stateAdapterInstance;
303
352
  _name = null;
353
+ _messageHistory;
304
354
  constructor(config) {
305
355
  this.id = config.id;
306
356
  this.isDM = config.isDM ?? false;
@@ -309,6 +359,7 @@ var ChannelImpl = class _ChannelImpl {
309
359
  } else {
310
360
  this._adapter = config.adapter;
311
361
  this._stateAdapterInstance = config.stateAdapter;
362
+ this._messageHistory = config.messageHistory;
312
363
  }
313
364
  }
314
365
  get adapter() {
@@ -362,14 +413,17 @@ var ChannelImpl = class _ChannelImpl {
362
413
  get messages() {
363
414
  const adapter = this.adapter;
364
415
  const channelId = this.id;
416
+ const messageHistory = this._messageHistory;
365
417
  return {
366
418
  async *[Symbol.asyncIterator]() {
367
419
  let cursor;
420
+ let yieldedAny = false;
368
421
  while (true) {
369
422
  const fetchOptions = { cursor, direction: "backward" };
370
423
  const result = adapter.fetchChannelMessages ? await adapter.fetchChannelMessages(channelId, fetchOptions) : await adapter.fetchMessages(channelId, fetchOptions);
371
424
  const reversed = [...result.messages].reverse();
372
425
  for (const message of reversed) {
426
+ yieldedAny = true;
373
427
  yield message;
374
428
  }
375
429
  if (!result.nextCursor || result.messages.length === 0) {
@@ -377,6 +431,12 @@ var ChannelImpl = class _ChannelImpl {
377
431
  }
378
432
  cursor = result.nextCursor;
379
433
  }
434
+ if (!yieldedAny && messageHistory) {
435
+ const cached = await messageHistory.getMessages(channelId);
436
+ for (let i = cached.length - 1; i >= 0; i--) {
437
+ yield cached[i];
438
+ }
439
+ }
380
440
  }
381
441
  };
382
442
  }
@@ -422,10 +482,12 @@ var ChannelImpl = class _ChannelImpl {
422
482
  async post(message) {
423
483
  if (isAsyncIterable(message)) {
424
484
  let accumulated = "";
425
- for await (const chunk of message) {
426
- accumulated += chunk;
485
+ for await (const chunk of fromFullStream(message)) {
486
+ if (typeof chunk === "string") {
487
+ accumulated += chunk;
488
+ }
427
489
  }
428
- return this.postSingleMessage(accumulated);
490
+ return this.postSingleMessage({ markdown: accumulated });
429
491
  }
430
492
  let postable = message;
431
493
  if (isJSX(message)) {
@@ -439,7 +501,15 @@ var ChannelImpl = class _ChannelImpl {
439
501
  }
440
502
  async postSingleMessage(postable) {
441
503
  const rawMessage = this.adapter.postChannelMessage ? await this.adapter.postChannelMessage(this.id, postable) : await this.adapter.postMessage(this.id, postable);
442
- return this.createSentMessage(rawMessage.id, postable, rawMessage.threadId);
504
+ const sent = this.createSentMessage(
505
+ rawMessage.id,
506
+ postable,
507
+ rawMessage.threadId
508
+ );
509
+ if (this._messageHistory) {
510
+ await this._messageHistory.append(this.id, new Message(sent));
511
+ }
512
+ return sent;
443
513
  }
444
514
  async postEphemeral(user, message, options) {
445
515
  const { fallbackToDM } = options;
@@ -472,6 +542,25 @@ var ChannelImpl = class _ChannelImpl {
472
542
  }
473
543
  return null;
474
544
  }
545
+ async schedule(message, options) {
546
+ let postable;
547
+ if (isJSX(message)) {
548
+ const card = toCardElement(message);
549
+ if (!card) {
550
+ throw new Error("Invalid JSX element: must be a Card element");
551
+ }
552
+ postable = card;
553
+ } else {
554
+ postable = message;
555
+ }
556
+ if (!this.adapter.scheduleMessage) {
557
+ throw new NotImplementedError(
558
+ "Scheduled messages are not supported by this adapter",
559
+ "scheduling"
560
+ );
561
+ }
562
+ return this.adapter.scheduleMessage(this.id, postable, options);
563
+ }
475
564
  async startTyping(status) {
476
565
  await this.adapter.startTyping(this.id, status);
477
566
  }
@@ -555,11 +644,7 @@ var ChannelImpl = class _ChannelImpl {
555
644
  }
556
645
  };
557
646
  function deriveChannelId(adapter, threadId) {
558
- if (adapter.channelIdFromThreadId) {
559
- return adapter.channelIdFromThreadId(threadId);
560
- }
561
- const parts = threadId.split(":");
562
- return parts.slice(0, 2).join(":");
647
+ return adapter.channelIdFromThreadId(threadId);
563
648
  }
564
649
  function extractMessageContent(message) {
565
650
  if (typeof message === "string") {
@@ -610,45 +695,49 @@ function extractMessageContent(message) {
610
695
  throw new Error("Invalid PostableMessage format");
611
696
  }
612
697
 
698
+ // src/message-history.ts
699
+ var DEFAULT_MAX_MESSAGES = 100;
700
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
701
+ var KEY_PREFIX = "msg-history:";
702
+ var MessageHistoryCache = class {
703
+ state;
704
+ maxMessages;
705
+ ttlMs;
706
+ constructor(state, config) {
707
+ this.state = state;
708
+ this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
709
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
710
+ }
711
+ /**
712
+ * Atomically append a message to the history for a thread.
713
+ * Trims to maxMessages (keeps newest) and refreshes TTL.
714
+ */
715
+ async append(threadId, message) {
716
+ const key = `${KEY_PREFIX}${threadId}`;
717
+ const serialized = message.toJSON();
718
+ serialized.raw = null;
719
+ await this.state.appendToList(key, serialized, {
720
+ maxLength: this.maxMessages,
721
+ ttlMs: this.ttlMs
722
+ });
723
+ }
724
+ /**
725
+ * Get messages for a thread in chronological order (oldest first).
726
+ *
727
+ * @param threadId - The thread ID
728
+ * @param limit - Optional limit on number of messages to return (returns newest N)
729
+ */
730
+ async getMessages(threadId, limit) {
731
+ const key = `${KEY_PREFIX}${threadId}`;
732
+ const stored = await this.state.getList(key);
733
+ const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
734
+ return sliced.map((s) => Message.fromJSON(s));
735
+ }
736
+ };
737
+
613
738
  // src/thread.ts
614
739
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
615
740
 
616
- // src/from-full-stream.ts
617
- var STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
618
- "markdown_text",
619
- "task_update",
620
- "plan_update"
621
- ]);
622
- async function* fromFullStream(stream) {
623
- let needsSeparator = false;
624
- let hasEmittedText = false;
625
- for await (const event of stream) {
626
- if (typeof event === "string") {
627
- yield event;
628
- continue;
629
- }
630
- if (event === null || typeof event !== "object" || !("type" in event)) {
631
- continue;
632
- }
633
- const typed = event;
634
- if (STREAM_CHUNK_TYPES.has(typed.type)) {
635
- yield event;
636
- continue;
637
- }
638
- const textContent = typed.text ?? typed.delta ?? typed.textDelta;
639
- if (typed.type === "text-delta" && typeof textContent === "string") {
640
- if (needsSeparator && hasEmittedText) {
641
- yield "\n\n";
642
- }
643
- needsSeparator = false;
644
- hasEmittedText = true;
645
- yield textContent;
646
- } else if (typed.type === "step-finish") {
647
- needsSeparator = true;
648
- }
649
- }
650
- }
651
-
652
741
  // src/streaming-markdown.ts
653
742
  import remend from "remend";
654
743
  var StreamingMarkdownRenderer = class {
@@ -902,6 +991,8 @@ var ThreadImpl = class _ThreadImpl {
902
991
  _fallbackStreamingPlaceholderText;
903
992
  /** Cached channel instance */
904
993
  _channel;
994
+ /** Message history cache (set only for adapters with persistMessageHistory) */
995
+ _messageHistory;
905
996
  constructor(config) {
906
997
  this.id = config.id;
907
998
  this.channelId = config.channelId;
@@ -915,6 +1006,7 @@ var ThreadImpl = class _ThreadImpl {
915
1006
  } else {
916
1007
  this._adapter = config.adapter;
917
1008
  this._stateAdapterInstance = config.stateAdapter;
1009
+ this._messageHistory = config.messageHistory;
918
1010
  }
919
1011
  if (config.initialMessage) {
920
1012
  this._recentMessages = [config.initialMessage];
@@ -993,7 +1085,8 @@ var ThreadImpl = class _ThreadImpl {
993
1085
  id: channelId,
994
1086
  adapter: this.adapter,
995
1087
  stateAdapter: this._stateAdapter,
996
- isDM: this.isDM
1088
+ isDM: this.isDM,
1089
+ messageHistory: this._messageHistory
997
1090
  });
998
1091
  }
999
1092
  return this._channel;
@@ -1005,9 +1098,11 @@ var ThreadImpl = class _ThreadImpl {
1005
1098
  get messages() {
1006
1099
  const adapter = this.adapter;
1007
1100
  const threadId = this.id;
1101
+ const messageHistory = this._messageHistory;
1008
1102
  return {
1009
1103
  async *[Symbol.asyncIterator]() {
1010
1104
  let cursor;
1105
+ let yieldedAny = false;
1011
1106
  while (true) {
1012
1107
  const result = await adapter.fetchMessages(threadId, {
1013
1108
  cursor,
@@ -1015,6 +1110,7 @@ var ThreadImpl = class _ThreadImpl {
1015
1110
  });
1016
1111
  const reversed = [...result.messages].reverse();
1017
1112
  for (const message of reversed) {
1113
+ yieldedAny = true;
1018
1114
  yield message;
1019
1115
  }
1020
1116
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1022,15 +1118,23 @@ var ThreadImpl = class _ThreadImpl {
1022
1118
  }
1023
1119
  cursor = result.nextCursor;
1024
1120
  }
1121
+ if (!yieldedAny && messageHistory) {
1122
+ const cached = await messageHistory.getMessages(threadId);
1123
+ for (let i = cached.length - 1; i >= 0; i--) {
1124
+ yield cached[i];
1125
+ }
1126
+ }
1025
1127
  }
1026
1128
  };
1027
1129
  }
1028
1130
  get allMessages() {
1029
1131
  const adapter = this.adapter;
1030
1132
  const threadId = this.id;
1133
+ const messageHistory = this._messageHistory;
1031
1134
  return {
1032
1135
  async *[Symbol.asyncIterator]() {
1033
1136
  let cursor;
1137
+ let yieldedAny = false;
1034
1138
  while (true) {
1035
1139
  const result = await adapter.fetchMessages(threadId, {
1036
1140
  limit: 100,
@@ -1038,6 +1142,7 @@ var ThreadImpl = class _ThreadImpl {
1038
1142
  direction: "forward"
1039
1143
  });
1040
1144
  for (const message of result.messages) {
1145
+ yieldedAny = true;
1041
1146
  yield message;
1042
1147
  }
1043
1148
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1045,6 +1150,12 @@ var ThreadImpl = class _ThreadImpl {
1045
1150
  }
1046
1151
  cursor = result.nextCursor;
1047
1152
  }
1153
+ if (!yieldedAny && messageHistory) {
1154
+ const cached = await messageHistory.getMessages(threadId);
1155
+ for (const message of cached) {
1156
+ yield message;
1157
+ }
1158
+ }
1048
1159
  }
1049
1160
  };
1050
1161
  }
@@ -1081,6 +1192,9 @@ var ThreadImpl = class _ThreadImpl {
1081
1192
  postable,
1082
1193
  rawMessage.threadId
1083
1194
  );
1195
+ if (this._messageHistory) {
1196
+ await this._messageHistory.append(this.id, new Message(result));
1197
+ }
1084
1198
  return result;
1085
1199
  }
1086
1200
  async postEphemeral(user, message, options) {
@@ -1114,6 +1228,25 @@ var ThreadImpl = class _ThreadImpl {
1114
1228
  }
1115
1229
  return null;
1116
1230
  }
1231
+ async schedule(message, options) {
1232
+ let postable;
1233
+ if (isJSX(message)) {
1234
+ const card = toCardElement(message);
1235
+ if (!card) {
1236
+ throw new Error("Invalid JSX element: must be a Card element");
1237
+ }
1238
+ postable = card;
1239
+ } else {
1240
+ postable = message;
1241
+ }
1242
+ if (!this.adapter.scheduleMessage) {
1243
+ throw new NotImplementedError(
1244
+ "Scheduled messages are not supported by this adapter",
1245
+ "scheduling"
1246
+ );
1247
+ }
1248
+ return this.adapter.scheduleMessage(this.id, postable, options);
1249
+ }
1117
1250
  /**
1118
1251
  * Handle streaming from an AsyncIterable.
1119
1252
  * Normalizes the stream (supports both textStream and fullStream from AI SDK),
@@ -1149,11 +1282,15 @@ var ThreadImpl = class _ThreadImpl {
1149
1282
  }
1150
1283
  };
1151
1284
  const raw = await this.adapter.stream(this.id, wrappedStream, options);
1152
- return this.createSentMessage(
1285
+ const sent = this.createSentMessage(
1153
1286
  raw.id,
1154
1287
  { markdown: accumulated },
1155
1288
  raw.threadId
1156
1289
  );
1290
+ if (this._messageHistory) {
1291
+ await this._messageHistory.append(this.id, new Message(sent));
1292
+ }
1293
+ return sent;
1157
1294
  }
1158
1295
  const textOnlyStream = {
1159
1296
  [Symbol.asyncIterator]: () => {
@@ -1265,15 +1402,28 @@ var ThreadImpl = class _ThreadImpl {
1265
1402
  markdown: accumulated
1266
1403
  });
1267
1404
  }
1268
- return this.createSentMessage(
1405
+ const sent = this.createSentMessage(
1269
1406
  msg.id,
1270
1407
  { markdown: accumulated },
1271
1408
  threadIdForEdits
1272
1409
  );
1410
+ if (this._messageHistory) {
1411
+ await this._messageHistory.append(this.id, new Message(sent));
1412
+ }
1413
+ return sent;
1273
1414
  }
1274
1415
  async refresh() {
1275
1416
  const result = await this.adapter.fetchMessages(this.id, { limit: 50 });
1276
- this._recentMessages = result.messages;
1417
+ if (result.messages.length > 0) {
1418
+ this._recentMessages = result.messages;
1419
+ } else if (this._messageHistory) {
1420
+ this._recentMessages = await this._messageHistory.getMessages(
1421
+ this.id,
1422
+ 50
1423
+ );
1424
+ } else {
1425
+ this._recentMessages = [];
1426
+ }
1277
1427
  }
1278
1428
  mentionUser(userId) {
1279
1429
  return `<@${userId}>`;
@@ -1529,7 +1679,10 @@ var Chat = class {
1529
1679
  _streamingUpdateIntervalMs;
1530
1680
  _fallbackStreamingPlaceholderText;
1531
1681
  _dedupeTtlMs;
1682
+ _onLockConflict;
1683
+ _messageHistory;
1532
1684
  mentionHandlers = [];
1685
+ directMessageHandlers = [];
1533
1686
  messagePatterns = [];
1534
1687
  subscribedMessageHandlers = [];
1535
1688
  reactionHandlers = [];
@@ -1557,6 +1710,11 @@ var Chat = class {
1557
1710
  this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
1558
1711
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
1559
1712
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1713
+ this._onLockConflict = config.onLockConflict;
1714
+ this._messageHistory = new MessageHistoryCache(
1715
+ this._stateAdapter,
1716
+ config.messageHistory
1717
+ );
1560
1718
  if (typeof config.logger === "string") {
1561
1719
  this.logger = new ConsoleLogger(config.logger);
1562
1720
  } else {
@@ -1663,6 +1821,27 @@ var Chat = class {
1663
1821
  this.mentionHandlers.push(handler);
1664
1822
  this.logger.debug("Registered mention handler");
1665
1823
  }
1824
+ /**
1825
+ * Register a handler for direct messages.
1826
+ *
1827
+ * Called when a message is received in a DM thread that is not subscribed.
1828
+ * If no `onDirectMessage` handlers are registered, DMs fall through to
1829
+ * `onNewMention` for backward compatibility.
1830
+ *
1831
+ * @param handler - Handler called for DM messages
1832
+ *
1833
+ * @example
1834
+ * ```typescript
1835
+ * chat.onDirectMessage(async (thread, message) => {
1836
+ * await thread.subscribe();
1837
+ * await thread.post("Thanks for the DM!");
1838
+ * });
1839
+ * ```
1840
+ */
1841
+ onDirectMessage(handler) {
1842
+ this.directMessageHandlers.push(handler);
1843
+ this.logger.debug("Registered direct message handler");
1844
+ }
1666
1845
  /**
1667
1846
  * Register a handler for messages matching a regex pattern.
1668
1847
  *
@@ -2489,15 +2668,34 @@ var Chat = class {
2489
2668
  });
2490
2669
  return;
2491
2670
  }
2492
- const lock = await this._stateAdapter.acquireLock(
2671
+ if (adapter.persistMessageHistory) {
2672
+ const channelId = adapter.channelIdFromThreadId(threadId);
2673
+ const appends = [this._messageHistory.append(threadId, message)];
2674
+ if (channelId !== threadId) {
2675
+ appends.push(this._messageHistory.append(channelId, message));
2676
+ }
2677
+ await Promise.all(appends);
2678
+ }
2679
+ let lock = await this._stateAdapter.acquireLock(
2493
2680
  threadId,
2494
2681
  DEFAULT_LOCK_TTL_MS
2495
2682
  );
2496
2683
  if (!lock) {
2497
- this.logger.warn("Could not acquire lock on thread", { threadId });
2498
- throw new LockError(
2499
- `Could not acquire lock on thread ${threadId}. Another instance may be processing.`
2500
- );
2684
+ const resolution = typeof this._onLockConflict === "function" ? await this._onLockConflict(threadId, message) : this._onLockConflict ?? "drop";
2685
+ if (resolution === "force") {
2686
+ this.logger.info("Force-releasing lock on thread", { threadId });
2687
+ await this._stateAdapter.forceReleaseLock(threadId);
2688
+ lock = await this._stateAdapter.acquireLock(
2689
+ threadId,
2690
+ DEFAULT_LOCK_TTL_MS
2691
+ );
2692
+ }
2693
+ if (!lock) {
2694
+ this.logger.warn("Could not acquire lock on thread", { threadId });
2695
+ throw new LockError(
2696
+ `Could not acquire lock on thread ${threadId}. Another instance may be processing.`
2697
+ );
2698
+ }
2501
2699
  }
2502
2700
  this.logger.debug("Lock acquired", { threadId, token: lock.token });
2503
2701
  try {
@@ -2514,6 +2712,21 @@ var Chat = class {
2514
2712
  message,
2515
2713
  isSubscribed
2516
2714
  );
2715
+ const isDM = adapter.isDM?.(threadId) ?? false;
2716
+ if (isDM && this.directMessageHandlers.length > 0) {
2717
+ this.logger.debug("Direct message received - calling handlers", {
2718
+ threadId,
2719
+ handlerCount: this.directMessageHandlers.length
2720
+ });
2721
+ const channel = thread.channel;
2722
+ for (const handler of this.directMessageHandlers) {
2723
+ await handler(thread, message, channel);
2724
+ }
2725
+ return;
2726
+ }
2727
+ if (isDM) {
2728
+ message.isMention = true;
2729
+ }
2517
2730
  if (isSubscribed) {
2518
2731
  this.logger.debug("Message in subscribed thread - calling handlers", {
2519
2732
  threadId,
@@ -2576,7 +2789,8 @@ var Chat = class {
2576
2789
  isDM,
2577
2790
  currentMessage: initialMessage,
2578
2791
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
2579
- fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText
2792
+ fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText,
2793
+ messageHistory: adapter.persistMessageHistory ? this._messageHistory : void 0
2580
2794
  });
2581
2795
  }
2582
2796
  /**
@@ -2875,6 +3089,8 @@ function convertEmojiPlaceholders(text2, platform, resolver = defaultEmojiResolv
2875
3089
  return resolver.toGChat(emojiName);
2876
3090
  case "linear":
2877
3091
  return resolver.toGChat(emojiName);
3092
+ case "whatsapp":
3093
+ return resolver.toGChat(emojiName);
2878
3094
  default:
2879
3095
  return resolver.toGChat(emojiName);
2880
3096
  }
@@ -3054,6 +3270,7 @@ export {
3054
3270
  LinkButton2 as LinkButton,
3055
3271
  LockError,
3056
3272
  Message,
3273
+ MessageHistoryCache,
3057
3274
  Modal2 as Modal,
3058
3275
  NotImplementedError,
3059
3276
  RadioSelect2 as RadioSelect,
@@ -3110,6 +3327,7 @@ export {
3110
3327
  tableElementToAscii,
3111
3328
  tableToAscii,
3112
3329
  text,
3330
+ toAiMessages,
3113
3331
  toCardElement2 as toCardElement,
3114
3332
  toModalElement2 as toModalElement,
3115
3333
  toPlainText,