chat 4.27.0 → 4.29.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 (49) hide show
  1. package/dist/ai/index.d.ts +501 -0
  2. package/dist/ai/index.js +500 -0
  3. package/dist/chat-D9UYaaNO.d.ts +3156 -0
  4. package/dist/chunk-HD375J7S.js +128 -0
  5. package/dist/{chunk-AN7MRAVW.js → chunk-V25FKIIL.js} +5 -1
  6. package/dist/index.d.ts +35 -2934
  7. package/dist/index.js +567 -210
  8. package/dist/{jsx-runtime-Co9uV6l7.d.ts → jsx-runtime-CFq1K_Ve.d.ts} +11 -1
  9. package/dist/jsx-runtime.d.ts +1 -1
  10. package/dist/jsx-runtime.js +1 -1
  11. package/docs/actions.mdx +52 -1
  12. package/docs/adapters.mdx +72 -36
  13. package/docs/ai/ai-sdk-tools.mdx +227 -0
  14. package/docs/ai/index.mdx +63 -0
  15. package/docs/ai/meta.json +4 -0
  16. package/docs/{api → ai}/to-ai-messages.mdx +16 -3
  17. package/docs/ai/types.mdx +243 -0
  18. package/docs/api/cards.mdx +4 -0
  19. package/docs/api/chat.mdx +132 -10
  20. package/docs/api/index.mdx +6 -6
  21. package/docs/api/markdown.mdx +28 -5
  22. package/docs/api/message.mdx +54 -1
  23. package/docs/api/meta.json +1 -0
  24. package/docs/api/modals.mdx +50 -0
  25. package/docs/api/postable-message.mdx +58 -4
  26. package/docs/api/thread.mdx +11 -3
  27. package/docs/api/transcripts.mdx +220 -0
  28. package/docs/cards.mdx +6 -0
  29. package/docs/concurrency.mdx +58 -15
  30. package/docs/contributing/building.mdx +74 -2
  31. package/docs/contributing/testing.mdx +4 -0
  32. package/docs/conversation-history.mdx +137 -0
  33. package/docs/direct-messages.mdx +23 -5
  34. package/docs/ephemeral-messages.mdx +1 -1
  35. package/docs/error-handling.mdx +15 -3
  36. package/docs/files.mdx +21 -1
  37. package/docs/handling-events.mdx +10 -7
  38. package/docs/index.mdx +8 -5
  39. package/docs/meta.json +17 -3
  40. package/docs/modals.mdx +24 -0
  41. package/docs/posting-messages.mdx +10 -4
  42. package/docs/slash-commands.mdx +4 -4
  43. package/docs/streaming.mdx +75 -27
  44. package/docs/subject.mdx +53 -0
  45. package/docs/testing.mdx +142 -0
  46. package/docs/threads-messages-channels.mdx +10 -1
  47. package/docs/usage.mdx +15 -2
  48. package/package.json +23 -2
  49. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +1 -1
package/dist/index.js CHANGED
@@ -60,135 +60,134 @@ import {
60
60
  toModalElement,
61
61
  toPlainText,
62
62
  walkAst
63
- } from "./chunk-AN7MRAVW.js";
63
+ } from "./chunk-V25FKIIL.js";
64
+ import {
65
+ toAiMessages
66
+ } from "./chunk-HD375J7S.js";
64
67
 
65
- // src/ai.ts
66
- var TEXT_MIME_PREFIXES = [
67
- "text/",
68
- "application/json",
69
- "application/xml",
70
- "application/javascript",
71
- "application/typescript",
72
- "application/yaml",
73
- "application/x-yaml",
74
- "application/toml"
75
- ];
76
- function isTextMimeType(mimeType) {
77
- return TEXT_MIME_PREFIXES.some(
78
- (prefix) => mimeType === prefix || mimeType.startsWith(prefix)
79
- );
68
+ // src/channel.ts
69
+ import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
70
+
71
+ // src/callback-url.ts
72
+ var CALLBACK_TOKEN_PREFIX = "__cb:";
73
+ var CALLBACK_CACHE_KEY_PREFIX = "chat:callback:";
74
+ var CALLBACK_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
75
+ function encodeCallbackValue(token) {
76
+ return `${CALLBACK_TOKEN_PREFIX}${token}`;
80
77
  }
81
- async function attachmentToPart(att) {
82
- if (att.type === "image") {
83
- if (att.fetchData) {
84
- try {
85
- const buffer = await att.fetchData();
86
- const mimeType = att.mimeType ?? "image/png";
87
- return {
88
- type: "file",
89
- data: `data:${mimeType};base64,${buffer.toString("base64")}`,
90
- mediaType: mimeType,
91
- filename: att.name
78
+ function decodeCallbackValue(value) {
79
+ if (!value?.startsWith(CALLBACK_TOKEN_PREFIX)) {
80
+ return { callbackToken: void 0 };
81
+ }
82
+ return { callbackToken: value.slice(CALLBACK_TOKEN_PREFIX.length) };
83
+ }
84
+ function generateToken() {
85
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
86
+ }
87
+ async function processActionsElement(actions, stateAdapter) {
88
+ return {
89
+ type: "actions",
90
+ children: await Promise.all(
91
+ actions.children.map(async (el) => {
92
+ if (el.type !== "button" || !el.callbackUrl) {
93
+ return el;
94
+ }
95
+ const token = generateToken();
96
+ const stored = {
97
+ url: el.callbackUrl,
98
+ originalValue: el.value
92
99
  };
93
- } catch (error) {
94
- console.error("toAiMessages: failed to fetch image data", error);
95
- return null;
100
+ await stateAdapter.set(
101
+ `${CALLBACK_CACHE_KEY_PREFIX}${token}`,
102
+ stored,
103
+ CALLBACK_TTL_MS
104
+ );
105
+ const processed = {
106
+ type: "button",
107
+ id: el.id,
108
+ label: el.label,
109
+ style: el.style,
110
+ disabled: el.disabled,
111
+ value: encodeCallbackValue(token),
112
+ actionType: el.actionType
113
+ };
114
+ return processed;
115
+ })
116
+ )
117
+ };
118
+ }
119
+ function hasCallbackButtons(children) {
120
+ for (const child of children) {
121
+ if (child.type === "actions") {
122
+ for (const el of child.children) {
123
+ if (el.type === "button" && el.callbackUrl) {
124
+ return true;
125
+ }
96
126
  }
97
127
  }
98
- return null;
128
+ if (child.type === "section" && "children" in child && hasCallbackButtons(child.children)) {
129
+ return true;
130
+ }
99
131
  }
100
- if (att.type === "file" && att.mimeType && isTextMimeType(att.mimeType)) {
101
- if (att.fetchData) {
102
- try {
103
- const buffer = await att.fetchData();
104
- return {
105
- type: "file",
106
- data: `data:${att.mimeType};base64,${buffer.toString("base64")}`,
107
- filename: att.name,
108
- mediaType: att.mimeType
109
- };
110
- } catch (error) {
111
- console.error("toAiMessages: failed to fetch file data", error);
112
- return null;
113
- }
132
+ return false;
133
+ }
134
+ async function processChildren(children, stateAdapter) {
135
+ const result = [];
136
+ for (const child of children) {
137
+ if (child.type === "actions") {
138
+ result.push(await processActionsElement(child, stateAdapter));
139
+ } else if (child.type === "section" && "children" in child) {
140
+ result.push({
141
+ ...child,
142
+ children: await processChildren(child.children, stateAdapter)
143
+ });
144
+ } else {
145
+ result.push(child);
114
146
  }
115
- return null;
116
147
  }
117
- return null;
148
+ return result;
118
149
  }
119
- async function toAiMessages(messages, options) {
120
- const includeNames = options?.includeNames ?? false;
121
- const transformMessage = options?.transformMessage;
122
- const onUnsupported = options?.onUnsupportedAttachment ?? ((att) => {
123
- console.warn(
124
- `toAiMessages: unsupported attachment type "${att.type}"${att.name ? ` (${att.name})` : ""} \u2014 skipped`
125
- );
126
- });
127
- const sorted = [...messages].sort(
128
- (a, b) => (a.metadata.dateSent?.getTime() ?? 0) - (b.metadata.dateSent?.getTime() ?? 0)
129
- );
130
- const filtered = sorted.filter((msg) => msg.text.trim());
131
- const results = await Promise.all(
132
- filtered.map(async (msg) => {
133
- const role = msg.author.isMe ? "assistant" : "user";
134
- let textContent = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
135
- if (msg.links && msg.links.length > 0) {
136
- const linkParts = msg.links.map((link2) => {
137
- const parts = link2.fetchMessage ? [`[Embedded message: ${link2.url}]`] : [link2.url];
138
- if (link2.title) {
139
- parts.push(`Title: ${link2.title}`);
140
- }
141
- if (link2.description) {
142
- parts.push(`Description: ${link2.description}`);
143
- }
144
- if (link2.siteName) {
145
- parts.push(`Site: ${link2.siteName}`);
146
- }
147
- return parts.join("\n");
148
- }).join("\n\n");
149
- textContent += `
150
-
151
- Links:
152
- ${linkParts}`;
153
- }
154
- let aiMessage;
155
- if (role === "user") {
156
- const attachmentParts = [];
157
- for (const att of msg.attachments ?? []) {
158
- const part = await attachmentToPart(att);
159
- if (part) {
160
- attachmentParts.push(part);
161
- } else if (att.type === "video" || att.type === "audio") {
162
- onUnsupported(att, msg);
163
- }
164
- }
165
- if (attachmentParts.length > 0) {
166
- aiMessage = {
167
- role,
168
- content: [
169
- { type: "text", text: textContent },
170
- ...attachmentParts
171
- ]
172
- };
173
- } else {
174
- aiMessage = { role, content: textContent };
175
- }
176
- } else {
177
- aiMessage = { role, content: textContent };
178
- }
179
- if (transformMessage) {
180
- return { result: await transformMessage(aiMessage, msg), source: msg };
181
- }
182
- return { result: aiMessage, source: msg };
183
- })
150
+ async function processCardCallbackUrls(card, stateAdapter) {
151
+ if (!hasCallbackButtons(card.children)) {
152
+ return card;
153
+ }
154
+ return {
155
+ ...card,
156
+ children: await processChildren(card.children, stateAdapter)
157
+ };
158
+ }
159
+ async function resolveCallbackUrl(token, stateAdapter) {
160
+ const stored = await stateAdapter.get(
161
+ `${CALLBACK_CACHE_KEY_PREFIX}${token}`
184
162
  );
185
- return results.filter(
186
- (r) => r.result != null
187
- ).map((r) => r.result);
163
+ if (!stored) {
164
+ return null;
165
+ }
166
+ if (typeof stored === "string") {
167
+ return { url: stored };
168
+ }
169
+ return stored;
170
+ }
171
+ async function postToCallbackUrl(callbackUrl, payload) {
172
+ try {
173
+ const response = await fetch(callbackUrl, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify(payload)
177
+ });
178
+ if (!response.ok) {
179
+ return {
180
+ error: new Error(
181
+ `Callback URL returned ${response.status}: ${await response.text().catch(() => "")}`
182
+ ),
183
+ status: response.status
184
+ };
185
+ }
186
+ return { status: response.status };
187
+ } catch (error) {
188
+ return { error };
189
+ }
188
190
  }
189
-
190
- // src/channel.ts
191
- import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
192
191
 
193
192
  // src/chat-singleton.ts
194
193
  var _singleton = null;
@@ -245,6 +244,10 @@ async function* fromFullStream(stream) {
245
244
 
246
245
  // src/message.ts
247
246
  import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
247
+ var adapterMap = /* @__PURE__ */ new WeakMap();
248
+ function setMessageAdapter(message, adapter) {
249
+ adapterMap.set(message, adapter);
250
+ }
248
251
  var Message = class _Message {
249
252
  /** Unique message ID */
250
253
  id;
@@ -283,8 +286,31 @@ var Message = class _Message {
283
286
  * ```
284
287
  */
285
288
  isMention;
289
+ /**
290
+ * Cross-platform user key for this message's author.
291
+ *
292
+ * Set by the Chat SDK before passing the message to handlers, when
293
+ * `ChatConfig.identity` is configured. `undefined` if no resolver is
294
+ * configured; `undefined` (i.e. absent) when the resolver returned null.
295
+ *
296
+ * Used by the Transcripts API to look up / append per-user transcripts.
297
+ */
298
+ userKey;
286
299
  /** Links found in the message */
287
300
  links;
301
+ _subjectPromise;
302
+ get subject() {
303
+ if (this._subjectPromise) {
304
+ return this._subjectPromise;
305
+ }
306
+ const adapter = adapterMap.get(this);
307
+ if (!adapter?.fetchSubject) {
308
+ this._subjectPromise = Promise.resolve(null);
309
+ return this._subjectPromise;
310
+ }
311
+ this._subjectPromise = adapter.fetchSubject(this.raw).catch(() => null);
312
+ return this._subjectPromise;
313
+ }
288
314
  constructor(data) {
289
315
  this.id = data.id;
290
316
  this.threadId = data.threadId;
@@ -497,7 +523,7 @@ var ChannelImpl = class _ChannelImpl {
497
523
  _adapterName;
498
524
  _stateAdapterInstance;
499
525
  _name = null;
500
- _messageHistory;
526
+ _threadHistory;
501
527
  constructor(config) {
502
528
  this.id = config.id;
503
529
  this.isDM = config.isDM ?? false;
@@ -507,7 +533,7 @@ var ChannelImpl = class _ChannelImpl {
507
533
  } else {
508
534
  this._adapter = config.adapter;
509
535
  this._stateAdapterInstance = config.stateAdapter;
510
- this._messageHistory = config.messageHistory;
536
+ this._threadHistory = config.threadHistory;
511
537
  }
512
538
  }
513
539
  get adapter() {
@@ -561,7 +587,7 @@ var ChannelImpl = class _ChannelImpl {
561
587
  get messages() {
562
588
  const adapter = this.adapter;
563
589
  const channelId = this.id;
564
- const messageHistory = this._messageHistory;
590
+ const threadHistory = this._threadHistory;
565
591
  return {
566
592
  async *[Symbol.asyncIterator]() {
567
593
  let cursor;
@@ -579,8 +605,8 @@ var ChannelImpl = class _ChannelImpl {
579
605
  }
580
606
  cursor = result.nextCursor;
581
607
  }
582
- if (!yieldedAny && messageHistory) {
583
- const cached = await messageHistory.getMessages(channelId);
608
+ if (!yieldedAny && threadHistory) {
609
+ const cached = await threadHistory.getMessages(channelId);
584
610
  for (let i = cached.length - 1; i >= 0; i--) {
585
611
  yield cached[i];
586
612
  }
@@ -637,6 +663,8 @@ var ChannelImpl = class _ChannelImpl {
637
663
  for await (const chunk of fromFullStream(message)) {
638
664
  if (typeof chunk === "string") {
639
665
  accumulated += chunk;
666
+ } else if (chunk.type === "markdown_text") {
667
+ accumulated += chunk.text;
640
668
  }
641
669
  }
642
670
  return this.postSingleMessage({ markdown: accumulated });
@@ -649,6 +677,7 @@ var ChannelImpl = class _ChannelImpl {
649
677
  }
650
678
  postable = card;
651
679
  }
680
+ postable = await this.processCallbackUrls(postable);
652
681
  return this.postSingleMessage(postable);
653
682
  }
654
683
  async handlePostableObject(obj) {
@@ -666,8 +695,8 @@ var ChannelImpl = class _ChannelImpl {
666
695
  postable,
667
696
  rawMessage.threadId
668
697
  );
669
- if (this._messageHistory) {
670
- await this._messageHistory.append(this.id, new Message(sent));
698
+ if (this._threadHistory) {
699
+ await this._threadHistory.append(this.id, new Message(sent));
671
700
  }
672
701
  return sent;
673
702
  }
@@ -684,6 +713,7 @@ var ChannelImpl = class _ChannelImpl {
684
713
  } else {
685
714
  postable = message;
686
715
  }
716
+ postable = await this.processCallbackUrls(postable);
687
717
  if (this.adapter.postEphemeral) {
688
718
  return this.adapter.postEphemeral(this.id, userId, postable);
689
719
  }
@@ -713,6 +743,7 @@ var ChannelImpl = class _ChannelImpl {
713
743
  } else {
714
744
  postable = message;
715
745
  }
746
+ postable = await this.processCallbackUrls(postable);
716
747
  if (!this.adapter.scheduleMessage) {
717
748
  throw new NotImplementedError(
718
749
  "Scheduled messages are not supported by this adapter",
@@ -721,6 +752,24 @@ var ChannelImpl = class _ChannelImpl {
721
752
  }
722
753
  return this.adapter.scheduleMessage(this.id, postable, options);
723
754
  }
755
+ async processCallbackUrls(postable) {
756
+ if (typeof postable === "string") {
757
+ return postable;
758
+ }
759
+ if ("type" in postable && postable.type === "card") {
760
+ return processCardCallbackUrls(postable, this._stateAdapter);
761
+ }
762
+ if ("card" in postable && postable.card?.type === "card") {
763
+ const processed = await processCardCallbackUrls(
764
+ postable.card,
765
+ this._stateAdapter
766
+ );
767
+ if (processed !== postable.card) {
768
+ return { ...postable, card: processed };
769
+ }
770
+ }
771
+ return postable;
772
+ }
724
773
  async startTyping(status) {
725
774
  await this.adapter.startTyping(this.id, status);
726
775
  }
@@ -790,6 +839,7 @@ var ChannelImpl = class _ChannelImpl {
790
839
  }
791
840
  editPostable = card;
792
841
  }
842
+ editPostable = await self.processCallbackUrls(editPostable);
793
843
  await adapter.editMessage(threadId, messageId, editPostable);
794
844
  return self.createSentMessage(messageId, editPostable);
795
845
  },
@@ -858,46 +908,6 @@ function extractMessageContent(message) {
858
908
  throw new Error("Invalid PostableMessage format");
859
909
  }
860
910
 
861
- // src/message-history.ts
862
- var DEFAULT_MAX_MESSAGES = 100;
863
- var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
864
- var KEY_PREFIX = "msg-history:";
865
- var MessageHistoryCache = class {
866
- state;
867
- maxMessages;
868
- ttlMs;
869
- constructor(state, config) {
870
- this.state = state;
871
- this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
872
- this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
873
- }
874
- /**
875
- * Atomically append a message to the history for a thread.
876
- * Trims to maxMessages (keeps newest) and refreshes TTL.
877
- */
878
- async append(threadId, message) {
879
- const key = `${KEY_PREFIX}${threadId}`;
880
- const serialized = message.toJSON();
881
- serialized.raw = null;
882
- await this.state.appendToList(key, serialized, {
883
- maxLength: this.maxMessages,
884
- ttlMs: this.ttlMs
885
- });
886
- }
887
- /**
888
- * Get messages for a thread in chronological order (oldest first).
889
- *
890
- * @param threadId - The thread ID
891
- * @param limit - Optional limit on number of messages to return (returns newest N)
892
- */
893
- async getMessages(threadId, limit) {
894
- const key = `${KEY_PREFIX}${threadId}`;
895
- const stored = await this.state.getList(key);
896
- const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
897
- return sliced.map((s) => Message.fromJSON(s));
898
- }
899
- };
900
-
901
911
  // src/thread.ts
902
912
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
903
913
 
@@ -1168,8 +1178,8 @@ var ThreadImpl = class _ThreadImpl {
1168
1178
  _fallbackStreamingPlaceholderText;
1169
1179
  /** Cached channel instance */
1170
1180
  _channel;
1171
- /** Message history cache (set only for adapters with persistMessageHistory) */
1172
- _messageHistory;
1181
+ /** Thread history cache (set only for adapters with persistThreadHistory) */
1182
+ _threadHistory;
1173
1183
  _logger;
1174
1184
  constructor(config) {
1175
1185
  this.id = config.id;
@@ -1186,7 +1196,7 @@ var ThreadImpl = class _ThreadImpl {
1186
1196
  } else {
1187
1197
  this._adapter = config.adapter;
1188
1198
  this._stateAdapterInstance = config.stateAdapter;
1189
- this._messageHistory = config.messageHistory;
1199
+ this._threadHistory = config.threadHistory;
1190
1200
  }
1191
1201
  if (config.initialMessage) {
1192
1202
  this._recentMessages = [config.initialMessage];
@@ -1267,7 +1277,7 @@ var ThreadImpl = class _ThreadImpl {
1267
1277
  stateAdapter: this._stateAdapter,
1268
1278
  isDM: this.isDM,
1269
1279
  channelVisibility: this.channelVisibility,
1270
- messageHistory: this._messageHistory
1280
+ threadHistory: this._threadHistory
1271
1281
  });
1272
1282
  }
1273
1283
  return this._channel;
@@ -1279,7 +1289,7 @@ var ThreadImpl = class _ThreadImpl {
1279
1289
  get messages() {
1280
1290
  const adapter = this.adapter;
1281
1291
  const threadId = this.id;
1282
- const messageHistory = this._messageHistory;
1292
+ const threadHistory = this._threadHistory;
1283
1293
  return {
1284
1294
  async *[Symbol.asyncIterator]() {
1285
1295
  let cursor;
@@ -1299,8 +1309,8 @@ var ThreadImpl = class _ThreadImpl {
1299
1309
  }
1300
1310
  cursor = result.nextCursor;
1301
1311
  }
1302
- if (!yieldedAny && messageHistory) {
1303
- const cached = await messageHistory.getMessages(threadId);
1312
+ if (!yieldedAny && threadHistory) {
1313
+ const cached = await threadHistory.getMessages(threadId);
1304
1314
  for (let i = cached.length - 1; i >= 0; i--) {
1305
1315
  yield cached[i];
1306
1316
  }
@@ -1311,7 +1321,7 @@ var ThreadImpl = class _ThreadImpl {
1311
1321
  get allMessages() {
1312
1322
  const adapter = this.adapter;
1313
1323
  const threadId = this.id;
1314
- const messageHistory = this._messageHistory;
1324
+ const threadHistory = this._threadHistory;
1315
1325
  return {
1316
1326
  async *[Symbol.asyncIterator]() {
1317
1327
  let cursor;
@@ -1331,8 +1341,8 @@ var ThreadImpl = class _ThreadImpl {
1331
1341
  }
1332
1342
  cursor = result.nextCursor;
1333
1343
  }
1334
- if (!yieldedAny && messageHistory) {
1335
- const cached = await messageHistory.getMessages(threadId);
1344
+ if (!yieldedAny && threadHistory) {
1345
+ const cached = await threadHistory.getMessages(threadId);
1336
1346
  for (const message of cached) {
1337
1347
  yield message;
1338
1348
  }
@@ -1394,14 +1404,15 @@ var ThreadImpl = class _ThreadImpl {
1394
1404
  }
1395
1405
  postable = card;
1396
1406
  }
1407
+ postable = await this.processCallbackUrls(postable);
1397
1408
  const rawMessage = await this.adapter.postMessage(this.id, postable);
1398
1409
  const result = this.createSentMessage(
1399
1410
  rawMessage.id,
1400
1411
  postable,
1401
1412
  rawMessage.threadId
1402
1413
  );
1403
- if (this._messageHistory) {
1404
- await this._messageHistory.append(this.id, new Message(result));
1414
+ if (this._threadHistory) {
1415
+ await this._threadHistory.append(this.id, new Message(result));
1405
1416
  }
1406
1417
  return result;
1407
1418
  }
@@ -1427,6 +1438,7 @@ var ThreadImpl = class _ThreadImpl {
1427
1438
  } else {
1428
1439
  postable = message;
1429
1440
  }
1441
+ postable = await this.processCallbackUrls(postable);
1430
1442
  if (this.adapter.postEphemeral) {
1431
1443
  return this.adapter.postEphemeral(this.id, userId, postable);
1432
1444
  }
@@ -1445,6 +1457,24 @@ var ThreadImpl = class _ThreadImpl {
1445
1457
  }
1446
1458
  return null;
1447
1459
  }
1460
+ async processCallbackUrls(postable) {
1461
+ if (typeof postable === "string") {
1462
+ return postable;
1463
+ }
1464
+ if ("type" in postable && postable.type === "card") {
1465
+ return processCardCallbackUrls(postable, this._stateAdapter);
1466
+ }
1467
+ if ("card" in postable && postable.card?.type === "card") {
1468
+ const processed = await processCardCallbackUrls(
1469
+ postable.card,
1470
+ this._stateAdapter
1471
+ );
1472
+ if (processed !== postable.card) {
1473
+ return { ...postable, card: processed };
1474
+ }
1475
+ }
1476
+ return postable;
1477
+ }
1448
1478
  async schedule(message, options) {
1449
1479
  let postable;
1450
1480
  if (isJSX(message)) {
@@ -1456,6 +1486,9 @@ var ThreadImpl = class _ThreadImpl {
1456
1486
  } else {
1457
1487
  postable = message;
1458
1488
  }
1489
+ postable = await this.processCallbackUrls(
1490
+ postable
1491
+ );
1459
1492
  if (!this.adapter.scheduleMessage) {
1460
1493
  throw new NotImplementedError(
1461
1494
  "Scheduled messages are not supported by this adapter",
@@ -1467,7 +1500,7 @@ var ThreadImpl = class _ThreadImpl {
1467
1500
  /**
1468
1501
  * Handle streaming from an AsyncIterable.
1469
1502
  * Normalizes the stream (supports both textStream and fullStream from AI SDK),
1470
- * then uses adapter's native streaming if available, otherwise falls back to post+edit.
1503
+ * then uses the adapter's stream implementation if available, otherwise falls back to post+edit.
1471
1504
  */
1472
1505
  async handleStream(rawStream, callerOptions) {
1473
1506
  const textStream = fromFullStream(rawStream);
@@ -1505,8 +1538,8 @@ var ThreadImpl = class _ThreadImpl {
1505
1538
  { markdown: accumulated },
1506
1539
  raw.threadId
1507
1540
  );
1508
- if (this._messageHistory) {
1509
- await this._messageHistory.append(this.id, new Message(sent));
1541
+ if (this._threadHistory) {
1542
+ await this._threadHistory.append(this.id, new Message(sent));
1510
1543
  }
1511
1544
  return sent;
1512
1545
  }
@@ -1653,8 +1686,8 @@ var ThreadImpl = class _ThreadImpl {
1653
1686
  { markdown: accumulated },
1654
1687
  threadIdForEdits
1655
1688
  );
1656
- if (this._messageHistory) {
1657
- await this._messageHistory.append(this.id, new Message(sent));
1689
+ if (this._threadHistory) {
1690
+ await this._threadHistory.append(this.id, new Message(sent));
1658
1691
  }
1659
1692
  return sent;
1660
1693
  }
@@ -1662,11 +1695,8 @@ var ThreadImpl = class _ThreadImpl {
1662
1695
  const result = await this.adapter.fetchMessages(this.id, { limit: 50 });
1663
1696
  if (result.messages.length > 0) {
1664
1697
  this._recentMessages = result.messages;
1665
- } else if (this._messageHistory) {
1666
- this._recentMessages = await this._messageHistory.getMessages(
1667
- this.id,
1668
- 50
1669
- );
1698
+ } else if (this._threadHistory) {
1699
+ this._recentMessages = await this._threadHistory.getMessages(this.id, 50);
1670
1700
  } else {
1671
1701
  this._recentMessages = [];
1672
1702
  }
@@ -1778,6 +1808,7 @@ var ThreadImpl = class _ThreadImpl {
1778
1808
  }
1779
1809
  postable2 = card;
1780
1810
  }
1811
+ postable2 = await self.processCallbackUrls(postable2);
1781
1812
  await adapter.editMessage(threadId, messageId, postable2);
1782
1813
  return self.createSentMessage(messageId, postable2);
1783
1814
  },
@@ -1821,6 +1852,7 @@ var ThreadImpl = class _ThreadImpl {
1821
1852
  }
1822
1853
  postable = card;
1823
1854
  }
1855
+ postable = await self.processCallbackUrls(postable);
1824
1856
  await adapter.editMessage(threadId, messageId, postable);
1825
1857
  return self.createSentMessage(messageId, postable, threadId);
1826
1858
  },
@@ -1902,6 +1934,177 @@ function reviver(_key, value) {
1902
1934
  return value;
1903
1935
  }
1904
1936
 
1937
+ // src/thread-history.ts
1938
+ var DEFAULT_MAX_MESSAGES = 100;
1939
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1940
+ var KEY_PREFIX = "msg-history:";
1941
+ var ThreadHistoryCache = class {
1942
+ state;
1943
+ maxMessages;
1944
+ ttlMs;
1945
+ constructor(state, config) {
1946
+ this.state = state;
1947
+ this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
1948
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
1949
+ }
1950
+ /**
1951
+ * Atomically append a message to the history for a thread.
1952
+ * Trims to maxMessages (keeps newest) and refreshes TTL.
1953
+ */
1954
+ async append(threadId, message) {
1955
+ const key = `${KEY_PREFIX}${threadId}`;
1956
+ const serialized = message.toJSON();
1957
+ serialized.raw = null;
1958
+ await this.state.appendToList(key, serialized, {
1959
+ maxLength: this.maxMessages,
1960
+ ttlMs: this.ttlMs
1961
+ });
1962
+ }
1963
+ /**
1964
+ * Get messages for a thread in chronological order (oldest first).
1965
+ *
1966
+ * @param threadId - The thread ID
1967
+ * @param limit - Optional limit on number of messages to return (returns newest N)
1968
+ */
1969
+ async getMessages(threadId, limit) {
1970
+ const key = `${KEY_PREFIX}${threadId}`;
1971
+ const stored = await this.state.getList(key);
1972
+ const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
1973
+ return sliced.map((s) => Message.fromJSON(s));
1974
+ }
1975
+ };
1976
+
1977
+ // src/transcripts.ts
1978
+ var KEY_PREFIX2 = "transcripts:user:";
1979
+ var DEFAULT_MAX_PER_USER = 200;
1980
+ var DEFAULT_LIST_LIMIT = 50;
1981
+ var DURATION_RE = /^(\d+)([smhd])$/;
1982
+ var TOMBSTONE_MARKER = "__chatSdkTombstone";
1983
+ function isTombstone(value) {
1984
+ return typeof value === "object" && value !== null && value[TOMBSTONE_MARKER] === true;
1985
+ }
1986
+ var MS_PER_UNIT = {
1987
+ s: 1e3,
1988
+ m: 6e4,
1989
+ h: 36e5,
1990
+ d: 864e5
1991
+ };
1992
+ var TranscriptsApiImpl = class {
1993
+ state;
1994
+ maxPerUser;
1995
+ retentionMs;
1996
+ storeFormatted;
1997
+ constructor(state, config) {
1998
+ this.state = state;
1999
+ this.maxPerUser = config.maxPerUser ?? DEFAULT_MAX_PER_USER;
2000
+ this.retentionMs = parseDuration(config.retention);
2001
+ this.storeFormatted = config.storeFormatted ?? false;
2002
+ }
2003
+ async append(thread, message, options) {
2004
+ const isMessage = message instanceof Message;
2005
+ let userKey;
2006
+ let role;
2007
+ let platformMessageId;
2008
+ if (isMessage) {
2009
+ userKey = message.userKey;
2010
+ role = "user";
2011
+ platformMessageId = message.id;
2012
+ if (!userKey) {
2013
+ return null;
2014
+ }
2015
+ } else {
2016
+ userKey = options?.userKey;
2017
+ role = message.role;
2018
+ platformMessageId = message.platformMessageId;
2019
+ if (!userKey) {
2020
+ throw new Error(
2021
+ "transcripts.append: options.userKey is required when appending an AppendInput"
2022
+ );
2023
+ }
2024
+ }
2025
+ const entry = {
2026
+ id: crypto.randomUUID(),
2027
+ userKey,
2028
+ role,
2029
+ text: message.text,
2030
+ platform: thread.adapter.name,
2031
+ threadId: thread.id,
2032
+ timestamp: Date.now()
2033
+ };
2034
+ if (this.storeFormatted && message.formatted) {
2035
+ entry.formatted = message.formatted;
2036
+ }
2037
+ if (platformMessageId !== void 0) {
2038
+ entry.platformMessageId = platformMessageId;
2039
+ }
2040
+ await this.state.appendToList(keyFor(userKey), entry, {
2041
+ maxLength: this.maxPerUser,
2042
+ ttlMs: this.retentionMs
2043
+ });
2044
+ return entry;
2045
+ }
2046
+ async list(query) {
2047
+ const raw = await this.state.getList(
2048
+ keyFor(query.userKey)
2049
+ );
2050
+ let filtered = raw.filter(
2051
+ (entry) => !isTombstone(entry)
2052
+ );
2053
+ if (query.platforms && query.platforms.length > 0) {
2054
+ const platforms = new Set(query.platforms);
2055
+ filtered = filtered.filter((m) => platforms.has(m.platform));
2056
+ }
2057
+ if (query.threadId !== void 0) {
2058
+ const tid = query.threadId;
2059
+ filtered = filtered.filter((m) => m.threadId === tid);
2060
+ }
2061
+ if (query.roles && query.roles.length > 0) {
2062
+ const roles = new Set(query.roles);
2063
+ filtered = filtered.filter((m) => roles.has(m.role));
2064
+ }
2065
+ const limit = query.limit ?? DEFAULT_LIST_LIMIT;
2066
+ if (filtered.length > limit) {
2067
+ filtered = filtered.slice(filtered.length - limit);
2068
+ }
2069
+ return filtered;
2070
+ }
2071
+ async count(query) {
2072
+ const raw = await this.state.getList(keyFor(query.userKey));
2073
+ return raw.filter((entry) => !isTombstone(entry)).length;
2074
+ }
2075
+ async delete(target) {
2076
+ const key = keyFor(target.userKey);
2077
+ const existing = await this.state.getList(key);
2078
+ const previous = existing.filter((entry) => !isTombstone(entry)).length;
2079
+ const tombstone = { [TOMBSTONE_MARKER]: true };
2080
+ await this.state.appendToList(key, tombstone, {
2081
+ maxLength: 1,
2082
+ ttlMs: this.retentionMs
2083
+ });
2084
+ return { deleted: previous };
2085
+ }
2086
+ };
2087
+ function keyFor(userKey) {
2088
+ return `${KEY_PREFIX2}${userKey}`;
2089
+ }
2090
+ function parseDuration(value) {
2091
+ if (value === void 0) {
2092
+ return void 0;
2093
+ }
2094
+ if (typeof value === "number") {
2095
+ return value;
2096
+ }
2097
+ const match = DURATION_RE.exec(value);
2098
+ if (!match) {
2099
+ throw new Error(
2100
+ `Invalid duration: ${value} (expected number of ms, or "<n>[smhd]")`
2101
+ );
2102
+ }
2103
+ const n = Number.parseInt(match[1], 10);
2104
+ const unit = match[2];
2105
+ return n * MS_PER_UNIT[unit];
2106
+ }
2107
+
1905
2108
  // src/chat.ts
1906
2109
  var DEFAULT_LOCK_TTL_MS = 3e4;
1907
2110
  function sleep(ms) {
@@ -1931,6 +2134,21 @@ var Chat = class {
1931
2134
  setChatSingleton(this);
1932
2135
  return this;
1933
2136
  }
2137
+ /**
2138
+ * Cross-platform per-user transcript store.
2139
+ *
2140
+ * Available only when `transcripts` is configured on the Chat instance
2141
+ * (and an `identity` resolver is set). Throws on access otherwise so
2142
+ * callers fail loudly rather than silently no-op'ing.
2143
+ */
2144
+ get transcripts() {
2145
+ if (!this._transcripts) {
2146
+ throw new Error(
2147
+ "chat.transcripts is not configured \u2014 pass `transcripts` and `identity` to ChatConfig to enable it"
2148
+ );
2149
+ }
2150
+ return this._transcripts;
2151
+ }
1934
2152
  /**
1935
2153
  * Get the registered singleton Chat instance.
1936
2154
  * Throws if no singleton has been registered.
@@ -1952,7 +2170,9 @@ var Chat = class {
1952
2170
  _fallbackStreamingPlaceholderText;
1953
2171
  _dedupeTtlMs;
1954
2172
  _onLockConflict;
1955
- _messageHistory;
2173
+ _threadHistory;
2174
+ _identity;
2175
+ _transcripts;
1956
2176
  _concurrencyStrategy;
1957
2177
  _concurrencyConfig;
1958
2178
  _concurrentSlots = /* @__PURE__ */ new Map();
@@ -2035,10 +2255,24 @@ var Chat = class {
2035
2255
  queueEntryTtlMs: 9e4
2036
2256
  };
2037
2257
  }
2038
- this._messageHistory = new MessageHistoryCache(
2258
+ this._threadHistory = new ThreadHistoryCache(
2039
2259
  this._stateAdapter,
2040
- config.messageHistory
2260
+ config.threadHistory ?? config.messageHistory
2041
2261
  );
2262
+ if (config.transcripts) {
2263
+ if (!config.identity) {
2264
+ throw new Error(
2265
+ "ChatConfig.transcripts requires ChatConfig.identity to be set \u2014 the cross-platform user key must be resolvable"
2266
+ );
2267
+ }
2268
+ this._identity = config.identity;
2269
+ this._transcripts = new TranscriptsApiImpl(
2270
+ this._stateAdapter,
2271
+ config.transcripts
2272
+ );
2273
+ } else {
2274
+ this._identity = config.identity;
2275
+ }
2042
2276
  const webhooks = {};
2043
2277
  for (const [name, adapter] of Object.entries(config.adapters)) {
2044
2278
  this.adapters.set(name, adapter);
@@ -2159,9 +2393,13 @@ var Chat = class {
2159
2393
  /**
2160
2394
  * Register a handler for direct messages.
2161
2395
  *
2162
- * Called when a message is received in a DM thread that is not subscribed.
2163
- * If no `onDirectMessage` handlers are registered, DMs fall through to
2164
- * `onNewMention` for backward compatibility.
2396
+ * Called for every message received in a DM thread when at least one
2397
+ * direct message handler is registered. Direct message handlers run before
2398
+ * `onSubscribedMessage`, `onNewMention`, and pattern handlers.
2399
+ *
2400
+ * If no `onDirectMessage` handlers are registered, DMs continue through
2401
+ * normal routing. Unsubscribed DMs fall through to `onNewMention` for
2402
+ * backward compatibility.
2165
2403
  *
2166
2404
  * @param handler - Handler called for DM messages
2167
2405
  *
@@ -2357,12 +2595,14 @@ var Chat = class {
2357
2595
  const task = (async () => {
2358
2596
  const message = typeof messageOrFactory === "function" ? await messageOrFactory() : messageOrFactory;
2359
2597
  await this.handleIncomingMessage(adapter, threadId, message);
2360
- })().catch((err) => {
2598
+ })();
2599
+ const tracked = task.catch((err) => {
2361
2600
  this.logger.error("Message processing error", { error: err, threadId });
2362
2601
  });
2363
2602
  if (options?.waitUntil) {
2364
- options.waitUntil(task);
2603
+ options.waitUntil(tracked);
2365
2604
  }
2605
+ return task;
2366
2606
  }
2367
2607
  /**
2368
2608
  * Process an incoming reaction event from an adapter.
@@ -2420,20 +2660,22 @@ var Chat = class {
2420
2660
  }
2421
2661
  }
2422
2662
  }
2423
- async processModalSubmit(event, contextId, _options) {
2424
- const { relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
2663
+ async processModalSubmit(event, contextId, options) {
2664
+ const { callbackUrl, relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
2425
2665
  const fullEvent = {
2426
2666
  ...event,
2427
2667
  relatedThread,
2428
2668
  relatedMessage,
2429
2669
  relatedChannel
2430
2670
  };
2671
+ let result;
2431
2672
  for (const { callbackIds, handler } of this.modalSubmitHandlers) {
2432
2673
  if (callbackIds.length === 0 || callbackIds.includes(event.callbackId)) {
2433
2674
  try {
2434
2675
  const response = await handler(fullEvent);
2435
2676
  if (response) {
2436
- return response;
2677
+ result = response;
2678
+ break;
2437
2679
  }
2438
2680
  } catch (err) {
2439
2681
  this.logger.error("Modal submit handler error", {
@@ -2443,6 +2685,30 @@ var Chat = class {
2443
2685
  }
2444
2686
  }
2445
2687
  }
2688
+ if (callbackUrl && result?.action !== "errors") {
2689
+ const task = postToCallbackUrl(callbackUrl, {
2690
+ type: "modal_submit",
2691
+ callbackId: event.callbackId,
2692
+ values: event.values,
2693
+ user: { id: event.user.userId, name: event.user.userName }
2694
+ }).then(({ error }) => {
2695
+ if (error) {
2696
+ this.logger.error("Modal callbackUrl POST failed", {
2697
+ callbackUrl,
2698
+ error
2699
+ });
2700
+ }
2701
+ }).catch((error) => {
2702
+ this.logger.error("Modal callbackUrl POST failed", {
2703
+ callbackUrl,
2704
+ error
2705
+ });
2706
+ });
2707
+ if (options?.waitUntil) {
2708
+ options.waitUntil(task);
2709
+ }
2710
+ }
2711
+ return result;
2446
2712
  }
2447
2713
  processModalClose(event, contextId, options) {
2448
2714
  const task = (async () => {
@@ -2594,7 +2860,8 @@ var Chat = class {
2594
2860
  contextId,
2595
2861
  void 0,
2596
2862
  void 0,
2597
- channel
2863
+ channel,
2864
+ modalElement.callbackUrl
2598
2865
  );
2599
2866
  if (options?.onOpenModal) {
2600
2867
  return options.onOpenModal(modalElement, contextId);
@@ -2631,12 +2898,13 @@ var Chat = class {
2631
2898
  * Store modal context server-side with a context ID.
2632
2899
  * Called when opening a modal to preserve thread/message/channel for the submit handler.
2633
2900
  */
2634
- async storeModalContext(adapterName, contextId, thread, message, channel) {
2901
+ async storeModalContext(adapterName, contextId, thread, message, channel, callbackUrl) {
2635
2902
  const key = `modal-context:${adapterName}:${contextId}`;
2636
2903
  const context = {
2637
2904
  thread: thread?.toJSON(),
2638
2905
  message: message?.toJSON(),
2639
- channel: channel?.toJSON()
2906
+ channel: channel?.toJSON(),
2907
+ callbackUrl
2640
2908
  };
2641
2909
  try {
2642
2910
  await this._stateAdapter.set(key, context, MODAL_CONTEXT_TTL_MS);
@@ -2654,6 +2922,7 @@ var Chat = class {
2654
2922
  async retrieveModalContext(adapterName, contextId) {
2655
2923
  if (!contextId) {
2656
2924
  return {
2925
+ callbackUrl: void 0,
2657
2926
  relatedThread: void 0,
2658
2927
  relatedMessage: void 0,
2659
2928
  relatedChannel: void 0
@@ -2663,6 +2932,7 @@ var Chat = class {
2663
2932
  const stored = await this._stateAdapter.get(key);
2664
2933
  if (!stored) {
2665
2934
  return {
2935
+ callbackUrl: void 0,
2666
2936
  relatedThread: void 0,
2667
2937
  relatedMessage: void 0,
2668
2938
  relatedChannel: void 0
@@ -2683,7 +2953,12 @@ var Chat = class {
2683
2953
  if (stored.channel) {
2684
2954
  relatedChannel = ChannelImpl.fromJSON(stored.channel, adapter);
2685
2955
  }
2686
- return { relatedThread, relatedMessage, relatedChannel };
2956
+ return {
2957
+ callbackUrl: stored.callbackUrl,
2958
+ relatedThread,
2959
+ relatedMessage,
2960
+ relatedChannel
2961
+ };
2687
2962
  }
2688
2963
  /**
2689
2964
  * Handle an action event internally.
@@ -2703,6 +2978,33 @@ var Chat = class {
2703
2978
  });
2704
2979
  return;
2705
2980
  }
2981
+ const { callbackToken } = decodeCallbackValue(event.value);
2982
+ let resolved = null;
2983
+ if (callbackToken) {
2984
+ resolved = await resolveCallbackUrl(callbackToken, this._stateAdapter);
2985
+ }
2986
+ const actionEvent = resolved ? { ...event, value: resolved.originalValue } : event;
2987
+ let callbackUrlPromise;
2988
+ if (resolved) {
2989
+ const callbackUrl = resolved.url;
2990
+ callbackUrlPromise = (async () => {
2991
+ const { error } = await postToCallbackUrl(callbackUrl, {
2992
+ type: "action",
2993
+ actionId: event.actionId,
2994
+ value: resolved.originalValue,
2995
+ user: { id: event.user.userId, name: event.user.userName },
2996
+ threadId: event.threadId,
2997
+ messageId: event.messageId
2998
+ });
2999
+ if (error) {
3000
+ this.logger.error("Button callbackUrl POST failed", {
3001
+ callbackUrl,
3002
+ actionId: event.actionId,
3003
+ error
3004
+ });
3005
+ }
3006
+ })();
3007
+ }
2706
3008
  const isSubscribed = false;
2707
3009
  const messageForThread = event.messageId ? new Message({
2708
3010
  id: event.messageId,
@@ -2721,7 +3023,7 @@ var Chat = class {
2721
3023
  isSubscribed
2722
3024
  ) : null;
2723
3025
  const fullEvent = {
2724
- ...event,
3026
+ ...actionEvent,
2725
3027
  thread,
2726
3028
  openModal: async (modal) => {
2727
3029
  if (!(event.triggerId || options?.onOpenModal)) {
@@ -2769,7 +3071,8 @@ var Chat = class {
2769
3071
  contextId,
2770
3072
  thread ? thread : void 0,
2771
3073
  message,
2772
- channel
3074
+ channel,
3075
+ modalElement.callbackUrl
2773
3076
  );
2774
3077
  if (options?.onOpenModal) {
2775
3078
  return options.onOpenModal(modalElement, contextId);
@@ -2801,6 +3104,9 @@ var Chat = class {
2801
3104
  await handler(fullEvent);
2802
3105
  }
2803
3106
  }
3107
+ if (callbackUrlPromise) {
3108
+ await callbackUrlPromise;
3109
+ }
2804
3110
  }
2805
3111
  /**
2806
3112
  * Handle a reaction event internally.
@@ -3110,9 +3416,10 @@ var Chat = class {
3110
3416
  * - Deduplication: Same message may arrive multiple times (e.g., Slack sends
3111
3417
  * both `message` and `app_mention` events, GChat sends direct webhook + Pub/Sub)
3112
3418
  * - Bot filtering: Messages from the bot itself are skipped
3113
- * - Concurrency: Controlled by `concurrency` config (drop, queue, debounce, concurrent)
3419
+ * - Concurrency: Controlled by `concurrency` config (drop, queue, debounce, burst, concurrent)
3114
3420
  */
3115
3421
  async handleIncomingMessage(adapter, threadId, message) {
3422
+ setMessageAdapter(message, adapter);
3116
3423
  this.logger.debug("Incoming message", {
3117
3424
  adapter: adapter.name,
3118
3425
  threadId,
@@ -3144,11 +3451,11 @@ var Chat = class {
3144
3451
  });
3145
3452
  return;
3146
3453
  }
3147
- if (adapter.persistMessageHistory) {
3454
+ if (adapter.persistThreadHistory || adapter.persistMessageHistory) {
3148
3455
  const channelId = adapter.channelIdFromThreadId(threadId);
3149
- const appends = [this._messageHistory.append(threadId, message)];
3456
+ const appends = [this._threadHistory.append(threadId, message)];
3150
3457
  if (channelId !== threadId) {
3151
- appends.push(this._messageHistory.append(channelId, message));
3458
+ appends.push(this._threadHistory.append(channelId, message));
3152
3459
  }
3153
3460
  await Promise.all(appends);
3154
3461
  }
@@ -3158,7 +3465,7 @@ var Chat = class {
3158
3465
  await this.handleConcurrent(adapter, threadId, message);
3159
3466
  return;
3160
3467
  }
3161
- if (strategy === "queue" || strategy === "debounce") {
3468
+ if (strategy === "queue" || strategy === "debounce" || strategy === "burst") {
3162
3469
  await this.handleQueueOrDebounce(
3163
3470
  adapter,
3164
3471
  threadId,
@@ -3277,6 +3584,25 @@ var Chat = class {
3277
3584
  debounceMs
3278
3585
  });
3279
3586
  await this.debounceLoop(lock, adapter, threadId, lockKey);
3587
+ } else if (strategy === "burst") {
3588
+ await this._stateAdapter.enqueue(
3589
+ lockKey,
3590
+ {
3591
+ message,
3592
+ enqueuedAt: Date.now(),
3593
+ expiresAt: Date.now() + queueEntryTtlMs
3594
+ },
3595
+ maxQueueSize
3596
+ );
3597
+ this.logger.info("message-debouncing", {
3598
+ threadId,
3599
+ lockKey,
3600
+ messageId: message.id,
3601
+ debounceMs
3602
+ });
3603
+ await sleep(debounceMs);
3604
+ await this._stateAdapter.extendLock(lock, DEFAULT_LOCK_TTL_MS);
3605
+ await this.drainQueue(lock, adapter, threadId, lockKey);
3280
3606
  } else {
3281
3607
  await this.dispatchToHandlers(adapter, threadId, message);
3282
3608
  await this.drainQueue(lock, adapter, threadId, lockKey);
@@ -3436,6 +3762,25 @@ var Chat = class {
3436
3762
  message,
3437
3763
  isSubscribed
3438
3764
  );
3765
+ if (this._identity && message.userKey === void 0) {
3766
+ try {
3767
+ const resolved = await this._identity({
3768
+ adapter: adapter.name,
3769
+ author: message.author,
3770
+ message
3771
+ });
3772
+ if (resolved) {
3773
+ message.userKey = resolved;
3774
+ }
3775
+ } catch (err) {
3776
+ this.logger.warn("Identity resolver threw; skipping userKey", {
3777
+ error: err,
3778
+ adapter: adapter.name,
3779
+ threadId,
3780
+ authorUserId: message.author.userId
3781
+ });
3782
+ }
3783
+ }
3439
3784
  const isDM = adapter.isDM?.(threadId) ?? false;
3440
3785
  if (isDM && this.directMessageHandlers.length > 0) {
3441
3786
  this.logger.debug("Direct message received - calling handlers", {
@@ -3517,7 +3862,7 @@ var Chat = class {
3517
3862
  logger: this.logger,
3518
3863
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
3519
3864
  fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText,
3520
- messageHistory: adapter.persistMessageHistory ? this._messageHistory : void 0
3865
+ threadHistory: adapter.persistThreadHistory || adapter.persistMessageHistory ? this._threadHistory : void 0
3521
3866
  });
3522
3867
  }
3523
3868
  /**
@@ -3563,6 +3908,9 @@ var Chat = class {
3563
3908
  */
3564
3909
  rehydrateMessage(raw, adapter) {
3565
3910
  if (raw instanceof Message) {
3911
+ if (adapter) {
3912
+ setMessageAdapter(raw, adapter);
3913
+ }
3566
3914
  return raw;
3567
3915
  }
3568
3916
  const obj = raw;
@@ -3592,6 +3940,9 @@ var Chat = class {
3592
3940
  links: obj.links ?? []
3593
3941
  });
3594
3942
  }
3943
+ if (adapter) {
3944
+ setMessageAdapter(msg, adapter);
3945
+ }
3595
3946
  const rehydrate = adapter?.rehydrateAttachment?.bind(adapter);
3596
3947
  if (rehydrate && msg.attachments.length > 0) {
3597
3948
  msg.attachments = msg.attachments.map(
@@ -3607,6 +3958,9 @@ var Chat = class {
3607
3958
  }
3608
3959
  };
3609
3960
 
3961
+ // src/message-history.ts
3962
+ var MessageHistoryCache = ThreadHistoryCache;
3963
+
3610
3964
  // src/plan.ts
3611
3965
  function contentToPlainText(content) {
3612
3966
  if (!content) {
@@ -4107,6 +4461,8 @@ function convertEmojiPlaceholders(text2, platform, resolver = defaultEmojiResolv
4107
4461
  return resolver.toGChat(emojiName);
4108
4462
  case "discord":
4109
4463
  return resolver.toDiscord(emojiName);
4464
+ case "messenger":
4465
+ return resolver.toGChat(emojiName);
4110
4466
  case "github":
4111
4467
  return resolver.toGChat(emojiName);
4112
4468
  case "linear":
@@ -4308,6 +4664,7 @@ export {
4308
4664
  THREAD_STATE_TTL_MS,
4309
4665
  Table2 as Table,
4310
4666
  TextInput2 as TextInput,
4667
+ ThreadHistoryCache,
4311
4668
  ThreadImpl,
4312
4669
  blockquote,
4313
4670
  cardChildToFallbackText2 as cardChildToFallbackText,