chat 4.21.0 → 4.23.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.
package/dist/index.js CHANGED
@@ -131,7 +131,7 @@ async function toAiMessages(messages, options) {
131
131
  filtered.map(async (msg) => {
132
132
  const role = msg.author.isMe ? "assistant" : "user";
133
133
  let textContent = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
134
- if (msg.links.length > 0) {
134
+ if (msg.links && msg.links.length > 0) {
135
135
  const linkParts = msg.links.map((link2) => {
136
136
  const parts = link2.fetchMessage ? [`[Embedded message: ${link2.url}]`] : [link2.url];
137
137
  if (link2.title) {
@@ -153,7 +153,7 @@ ${linkParts}`;
153
153
  let aiMessage;
154
154
  if (role === "user") {
155
155
  const attachmentParts = [];
156
- for (const att of msg.attachments) {
156
+ for (const att of msg.attachments ?? []) {
157
157
  const part = await attachmentToPart(att);
158
158
  if (part) {
159
159
  attachmentParts.push(part);
@@ -469,6 +469,7 @@ function isAsyncIterable(value) {
469
469
  var ChannelImpl = class _ChannelImpl {
470
470
  id;
471
471
  isDM;
472
+ channelVisibility;
472
473
  _adapter;
473
474
  _adapterName;
474
475
  _stateAdapterInstance;
@@ -477,6 +478,7 @@ var ChannelImpl = class _ChannelImpl {
477
478
  constructor(config) {
478
479
  this.id = config.id;
479
480
  this.isDM = config.isDM ?? false;
481
+ this.channelVisibility = config.channelVisibility ?? "unknown";
480
482
  if (isLazyConfig(config)) {
481
483
  this._adapterName = config.adapterName;
482
484
  } else {
@@ -695,6 +697,7 @@ var ChannelImpl = class _ChannelImpl {
695
697
  _type: "chat:Channel",
696
698
  id: this.id,
697
699
  adapterName: this.adapter.name,
700
+ channelVisibility: this.channelVisibility,
698
701
  isDM: this.isDM
699
702
  };
700
703
  }
@@ -702,6 +705,7 @@ var ChannelImpl = class _ChannelImpl {
702
705
  const channel = new _ChannelImpl({
703
706
  id: json.id,
704
707
  adapterName: json.adapterName,
708
+ channelVisibility: json.channelVisibility,
705
709
  isDM: json.isDM
706
710
  });
707
711
  if (adapter) {
@@ -1099,6 +1103,7 @@ var ThreadImpl = class _ThreadImpl {
1099
1103
  id;
1100
1104
  channelId;
1101
1105
  isDM;
1106
+ channelVisibility;
1102
1107
  /** Direct adapter instance (if provided) */
1103
1108
  _adapter;
1104
1109
  /** Adapter name for lazy resolution */
@@ -1122,6 +1127,7 @@ var ThreadImpl = class _ThreadImpl {
1122
1127
  this.id = config.id;
1123
1128
  this.channelId = config.channelId;
1124
1129
  this.isDM = config.isDM ?? false;
1130
+ this.channelVisibility = config.channelVisibility ?? "unknown";
1125
1131
  this._isSubscribedContext = config.isSubscribedContext ?? false;
1126
1132
  this._currentMessage = config.currentMessage;
1127
1133
  this._logger = config.logger;
@@ -1212,6 +1218,7 @@ var ThreadImpl = class _ThreadImpl {
1212
1218
  adapter: this.adapter,
1213
1219
  stateAdapter: this._stateAdapter,
1214
1220
  isDM: this.isDM,
1221
+ channelVisibility: this.channelVisibility,
1215
1222
  messageHistory: this._messageHistory
1216
1223
  });
1217
1224
  }
@@ -1573,6 +1580,7 @@ var ThreadImpl = class _ThreadImpl {
1573
1580
  _type: "chat:Thread",
1574
1581
  id: this.id,
1575
1582
  channelId: this.channelId,
1583
+ channelVisibility: this.channelVisibility,
1576
1584
  currentMessage: this._currentMessage?.toJSON(),
1577
1585
  isDM: this.isDM,
1578
1586
  adapterName: this.adapter.name
@@ -1597,6 +1605,7 @@ var ThreadImpl = class _ThreadImpl {
1597
1605
  id: json.id,
1598
1606
  adapterName: json.adapterName,
1599
1607
  channelId: json.channelId,
1608
+ channelVisibility: json.channelVisibility,
1600
1609
  currentMessage: json.currentMessage ? Message.fromJSON(json.currentMessage) : void 0,
1601
1610
  isDM: json.isDM
1602
1611
  });
@@ -1766,6 +1775,9 @@ function extractMessageContent2(message) {
1766
1775
 
1767
1776
  // src/chat.ts
1768
1777
  var DEFAULT_LOCK_TTL_MS = 3e4;
1778
+ function sleep(ms) {
1779
+ return new Promise((resolve) => setTimeout(resolve, ms));
1780
+ }
1769
1781
  var SLACK_USER_ID_REGEX = /^U[A-Z0-9]+$/i;
1770
1782
  var DISCORD_SNOWFLAKE_REGEX = /^\d{17,19}$/;
1771
1783
  var DEDUPE_TTL_MS = 5 * 60 * 1e3;
@@ -1810,6 +1822,9 @@ var Chat = class {
1810
1822
  _dedupeTtlMs;
1811
1823
  _onLockConflict;
1812
1824
  _messageHistory;
1825
+ _concurrencyStrategy;
1826
+ _concurrencyConfig;
1827
+ _lockScope;
1813
1828
  mentionHandlers = [];
1814
1829
  directMessageHandlers = [];
1815
1830
  messagePatterns = [];
@@ -1840,6 +1855,38 @@ var Chat = class {
1840
1855
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
1841
1856
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1842
1857
  this._onLockConflict = config.onLockConflict;
1858
+ this._lockScope = config.lockScope;
1859
+ const concurrency = config.concurrency;
1860
+ if (concurrency) {
1861
+ if (typeof concurrency === "string") {
1862
+ this._concurrencyStrategy = concurrency;
1863
+ this._concurrencyConfig = {
1864
+ debounceMs: 1500,
1865
+ maxConcurrent: Number.POSITIVE_INFINITY,
1866
+ maxQueueSize: 10,
1867
+ onQueueFull: "drop-oldest",
1868
+ queueEntryTtlMs: 9e4
1869
+ };
1870
+ } else {
1871
+ this._concurrencyStrategy = concurrency.strategy;
1872
+ this._concurrencyConfig = {
1873
+ debounceMs: concurrency.debounceMs ?? 1500,
1874
+ maxConcurrent: concurrency.maxConcurrent ?? Number.POSITIVE_INFINITY,
1875
+ maxQueueSize: concurrency.maxQueueSize ?? 10,
1876
+ onQueueFull: concurrency.onQueueFull ?? "drop-oldest",
1877
+ queueEntryTtlMs: concurrency.queueEntryTtlMs ?? 9e4
1878
+ };
1879
+ }
1880
+ } else {
1881
+ this._concurrencyStrategy = "drop";
1882
+ this._concurrencyConfig = {
1883
+ debounceMs: 1500,
1884
+ maxConcurrent: Number.POSITIVE_INFINITY,
1885
+ maxQueueSize: 10,
1886
+ onQueueFull: "drop-oldest",
1887
+ queueEntryTtlMs: 9e4
1888
+ };
1889
+ }
1843
1890
  this._messageHistory = new MessageHistoryCache(
1844
1891
  this._stateAdapter,
1845
1892
  config.messageHistory
@@ -2771,6 +2818,27 @@ var Chat = class {
2771
2818
  "UNKNOWN_USER_ID_FORMAT"
2772
2819
  );
2773
2820
  }
2821
+ /**
2822
+ * Resolve the lock key for a message based on lock scope.
2823
+ * With 'thread' scope, returns threadId. With 'channel' scope,
2824
+ * returns channelId (derived via adapter.channelIdFromThreadId).
2825
+ */
2826
+ async getLockKey(adapter, threadId) {
2827
+ const channelId = adapter.channelIdFromThreadId(threadId);
2828
+ let scope;
2829
+ if (typeof this._lockScope === "function") {
2830
+ const isDM = adapter.isDM?.(threadId) ?? false;
2831
+ scope = await this._lockScope({
2832
+ adapter,
2833
+ channelId,
2834
+ isDM,
2835
+ threadId
2836
+ });
2837
+ } else {
2838
+ scope = this._lockScope ?? adapter.lockScope ?? "thread";
2839
+ }
2840
+ return scope === "channel" ? channelId : threadId;
2841
+ }
2774
2842
  /**
2775
2843
  * Handle an incoming message from an adapter.
2776
2844
  * This is called by adapters when they receive a webhook.
@@ -2779,7 +2847,7 @@ var Chat = class {
2779
2847
  * - Deduplication: Same message may arrive multiple times (e.g., Slack sends
2780
2848
  * both `message` and `app_mention` events, GChat sends direct webhook + Pub/Sub)
2781
2849
  * - Bot filtering: Messages from the bot itself are skipped
2782
- * - Locking: Only one instance processes a thread at a time
2850
+ * - Concurrency: Controlled by `concurrency` config (drop, queue, debounce, concurrent)
2783
2851
  */
2784
2852
  async handleIncomingMessage(adapter, threadId, message) {
2785
2853
  this.logger.debug("Incoming message", {
@@ -2821,109 +2889,319 @@ var Chat = class {
2821
2889
  }
2822
2890
  await Promise.all(appends);
2823
2891
  }
2892
+ const lockKey = await this.getLockKey(adapter, threadId);
2893
+ const strategy = this._concurrencyStrategy;
2894
+ if (strategy === "concurrent") {
2895
+ await this.handleConcurrent(adapter, threadId, message);
2896
+ return;
2897
+ }
2898
+ if (strategy === "queue" || strategy === "debounce") {
2899
+ await this.handleQueueOrDebounce(
2900
+ adapter,
2901
+ threadId,
2902
+ lockKey,
2903
+ message,
2904
+ strategy
2905
+ );
2906
+ return;
2907
+ }
2908
+ await this.handleDrop(adapter, threadId, lockKey, message);
2909
+ }
2910
+ /**
2911
+ * Drop strategy: acquire lock or fail. Original behavior.
2912
+ */
2913
+ async handleDrop(adapter, threadId, lockKey, message) {
2824
2914
  let lock = await this._stateAdapter.acquireLock(
2825
- threadId,
2915
+ lockKey,
2826
2916
  DEFAULT_LOCK_TTL_MS
2827
2917
  );
2828
2918
  if (!lock) {
2829
2919
  const resolution = typeof this._onLockConflict === "function" ? await this._onLockConflict(threadId, message) : this._onLockConflict ?? "drop";
2830
2920
  if (resolution === "force") {
2831
- this.logger.info("Force-releasing lock on thread", { threadId });
2832
- await this._stateAdapter.forceReleaseLock(threadId);
2833
- lock = await this._stateAdapter.acquireLock(
2921
+ this.logger.info("Force-releasing lock on thread", {
2834
2922
  threadId,
2923
+ lockKey
2924
+ });
2925
+ await this._stateAdapter.forceReleaseLock(lockKey);
2926
+ lock = await this._stateAdapter.acquireLock(
2927
+ lockKey,
2835
2928
  DEFAULT_LOCK_TTL_MS
2836
2929
  );
2837
2930
  }
2838
2931
  if (!lock) {
2839
- this.logger.warn("Could not acquire lock on thread", { threadId });
2932
+ this.logger.warn("Could not acquire lock on thread", {
2933
+ threadId,
2934
+ lockKey
2935
+ });
2840
2936
  throw new LockError(
2841
2937
  `Could not acquire lock on thread ${threadId}. Another instance may be processing.`
2842
2938
  );
2843
2939
  }
2844
2940
  }
2845
- this.logger.debug("Lock acquired", { threadId, token: lock.token });
2941
+ this.logger.debug("Lock acquired", {
2942
+ threadId,
2943
+ lockKey,
2944
+ token: lock.token
2945
+ });
2846
2946
  try {
2847
- message.isMention = message.isMention || this.detectMention(adapter, message);
2848
- const isSubscribed = await this._stateAdapter.isSubscribed(threadId);
2849
- this.logger.debug("Subscription check", {
2850
- threadId,
2851
- isSubscribed,
2852
- subscribedHandlerCount: this.subscribedMessageHandlers.length
2853
- });
2854
- const thread = await this.createThread(
2855
- adapter,
2856
- threadId,
2857
- message,
2858
- isSubscribed
2859
- );
2860
- const isDM = adapter.isDM?.(threadId) ?? false;
2861
- if (isDM && this.directMessageHandlers.length > 0) {
2862
- this.logger.debug("Direct message received - calling handlers", {
2947
+ await this.dispatchToHandlers(adapter, threadId, message);
2948
+ } finally {
2949
+ await this._stateAdapter.releaseLock(lock);
2950
+ this.logger.debug("Lock released", { threadId, lockKey });
2951
+ }
2952
+ }
2953
+ /**
2954
+ * Queue/Debounce strategy: enqueue if lock is busy, drain after processing.
2955
+ */
2956
+ async handleQueueOrDebounce(adapter, threadId, lockKey, message, strategy) {
2957
+ const { maxQueueSize, queueEntryTtlMs, onQueueFull, debounceMs } = this._concurrencyConfig;
2958
+ const lock = await this._stateAdapter.acquireLock(
2959
+ lockKey,
2960
+ DEFAULT_LOCK_TTL_MS
2961
+ );
2962
+ if (!lock) {
2963
+ const effectiveMaxSize = strategy === "debounce" ? 1 : maxQueueSize;
2964
+ const depth = await this._stateAdapter.queueDepth(lockKey);
2965
+ if (depth >= effectiveMaxSize && strategy !== "debounce" && onQueueFull === "drop-newest") {
2966
+ this.logger.info("message-dropped", {
2863
2967
  threadId,
2864
- handlerCount: this.directMessageHandlers.length
2968
+ lockKey,
2969
+ messageId: message.id,
2970
+ reason: "queue-full"
2865
2971
  });
2866
- const channel = thread.channel;
2867
- for (const handler of this.directMessageHandlers) {
2868
- await handler(thread, message, channel);
2869
- }
2870
2972
  return;
2871
2973
  }
2872
- if (isDM) {
2873
- message.isMention = true;
2974
+ await this._stateAdapter.enqueue(
2975
+ lockKey,
2976
+ {
2977
+ message,
2978
+ enqueuedAt: Date.now(),
2979
+ expiresAt: Date.now() + queueEntryTtlMs
2980
+ },
2981
+ effectiveMaxSize
2982
+ );
2983
+ this.logger.info(
2984
+ strategy === "debounce" ? "message-debounce-reset" : "message-queued",
2985
+ {
2986
+ threadId,
2987
+ lockKey,
2988
+ messageId: message.id,
2989
+ queueDepth: Math.min(depth + 1, effectiveMaxSize)
2990
+ }
2991
+ );
2992
+ return;
2993
+ }
2994
+ this.logger.debug("Lock acquired", {
2995
+ threadId,
2996
+ lockKey,
2997
+ token: lock.token
2998
+ });
2999
+ try {
3000
+ if (strategy === "debounce") {
3001
+ await this._stateAdapter.enqueue(
3002
+ lockKey,
3003
+ {
3004
+ message,
3005
+ enqueuedAt: Date.now(),
3006
+ expiresAt: Date.now() + queueEntryTtlMs
3007
+ },
3008
+ 1
3009
+ );
3010
+ this.logger.info("message-debouncing", {
3011
+ threadId,
3012
+ lockKey,
3013
+ messageId: message.id,
3014
+ debounceMs
3015
+ });
3016
+ await this.debounceLoop(lock, adapter, threadId, lockKey);
3017
+ } else {
3018
+ await this.dispatchToHandlers(adapter, threadId, message);
3019
+ await this.drainQueue(lock, adapter, threadId, lockKey);
2874
3020
  }
2875
- if (isSubscribed) {
2876
- this.logger.debug("Message in subscribed thread - calling handlers", {
3021
+ } finally {
3022
+ await this._stateAdapter.releaseLock(lock);
3023
+ this.logger.debug("Lock released", { threadId, lockKey });
3024
+ }
3025
+ }
3026
+ /**
3027
+ * Debounce loop: wait for debounceMs, check if newer message arrived,
3028
+ * repeat until no new messages, then process the final message.
3029
+ */
3030
+ async debounceLoop(lock, adapter, threadId, lockKey) {
3031
+ const { debounceMs } = this._concurrencyConfig;
3032
+ while (true) {
3033
+ await sleep(debounceMs);
3034
+ await this._stateAdapter.extendLock(lock, DEFAULT_LOCK_TTL_MS);
3035
+ const entry = await this._stateAdapter.dequeue(lockKey);
3036
+ if (!entry) {
3037
+ break;
3038
+ }
3039
+ const msg = this.rehydrateMessage(entry.message);
3040
+ if (Date.now() > entry.expiresAt) {
3041
+ this.logger.info("message-expired", {
2877
3042
  threadId,
2878
- handlerCount: this.subscribedMessageHandlers.length
3043
+ lockKey,
3044
+ messageId: msg.id
2879
3045
  });
2880
- await this.runHandlers(this.subscribedMessageHandlers, thread, message);
2881
- return;
3046
+ continue;
2882
3047
  }
2883
- if (message.isMention) {
2884
- this.logger.debug("Bot mentioned", {
3048
+ const depth = await this._stateAdapter.queueDepth(lockKey);
3049
+ if (depth > 0) {
3050
+ this.logger.info("message-superseded", {
2885
3051
  threadId,
2886
- text: message.text.slice(0, 100)
3052
+ lockKey,
3053
+ droppedId: msg.id
2887
3054
  });
2888
- await this.runHandlers(this.mentionHandlers, thread, message);
2889
- return;
3055
+ continue;
2890
3056
  }
2891
- this.logger.debug("Checking message patterns", {
2892
- patternCount: this.messagePatterns.length,
2893
- patterns: this.messagePatterns.map((p) => p.pattern.toString()),
2894
- messageText: message.text
3057
+ this.logger.info("message-dequeued", {
3058
+ threadId,
3059
+ lockKey,
3060
+ messageId: msg.id
2895
3061
  });
2896
- let matchedPattern = false;
2897
- for (const { pattern, handler } of this.messagePatterns) {
2898
- const matches = pattern.test(message.text);
2899
- this.logger.debug("Pattern test", {
2900
- pattern: pattern.toString(),
2901
- text: message.text,
2902
- matches
2903
- });
2904
- if (matches) {
2905
- this.logger.debug("Message matched pattern - calling handler", {
2906
- pattern: pattern.toString()
3062
+ await this.dispatchToHandlers(adapter, threadId, msg);
3063
+ break;
3064
+ }
3065
+ }
3066
+ /**
3067
+ * Drain queue: collect all pending messages, dispatch the latest with
3068
+ * skipped context, then check for more.
3069
+ */
3070
+ async drainQueue(lock, adapter, threadId, lockKey) {
3071
+ while (true) {
3072
+ const pending = [];
3073
+ while (true) {
3074
+ const entry = await this._stateAdapter.dequeue(lockKey);
3075
+ if (!entry) {
3076
+ break;
3077
+ }
3078
+ const msg = this.rehydrateMessage(entry.message);
3079
+ if (Date.now() <= entry.expiresAt) {
3080
+ pending.push({ message: msg, expiresAt: entry.expiresAt });
3081
+ } else {
3082
+ this.logger.info("message-expired", {
3083
+ threadId,
3084
+ lockKey,
3085
+ messageId: msg.id
2907
3086
  });
2908
- matchedPattern = true;
2909
- await handler(thread, message);
2910
3087
  }
2911
3088
  }
2912
- if (!matchedPattern) {
2913
- this.logger.debug("No handlers matched message", {
2914
- threadId,
2915
- text: message.text.slice(0, 100)
3089
+ if (pending.length === 0) {
3090
+ return;
3091
+ }
3092
+ await this._stateAdapter.extendLock(lock, DEFAULT_LOCK_TTL_MS);
3093
+ const latest = pending.at(-1);
3094
+ if (!latest) {
3095
+ return;
3096
+ }
3097
+ const skipped = pending.slice(0, -1).map((e) => e.message);
3098
+ this.logger.info("message-dequeued", {
3099
+ threadId,
3100
+ lockKey,
3101
+ messageId: latest.message.id,
3102
+ skippedCount: skipped.length,
3103
+ totalSinceLastHandler: pending.length
3104
+ });
3105
+ const context = {
3106
+ skipped,
3107
+ totalSinceLastHandler: pending.length
3108
+ };
3109
+ await this.dispatchToHandlers(adapter, threadId, latest.message, context);
3110
+ }
3111
+ }
3112
+ /**
3113
+ * Concurrent strategy: no locking, process immediately.
3114
+ */
3115
+ async handleConcurrent(adapter, threadId, message) {
3116
+ await this.dispatchToHandlers(adapter, threadId, message);
3117
+ }
3118
+ /**
3119
+ * Dispatch a message to the appropriate handler chain based on
3120
+ * subscription status, mention detection, and pattern matching.
3121
+ */
3122
+ async dispatchToHandlers(adapter, threadId, message, context) {
3123
+ message.isMention = message.isMention || this.detectMention(adapter, message);
3124
+ const isSubscribed = await this._stateAdapter.isSubscribed(threadId);
3125
+ this.logger.debug("Subscription check", {
3126
+ threadId,
3127
+ isSubscribed,
3128
+ subscribedHandlerCount: this.subscribedMessageHandlers.length
3129
+ });
3130
+ const thread = await this.createThread(
3131
+ adapter,
3132
+ threadId,
3133
+ message,
3134
+ isSubscribed
3135
+ );
3136
+ const isDM = adapter.isDM?.(threadId) ?? false;
3137
+ if (isDM && this.directMessageHandlers.length > 0) {
3138
+ this.logger.debug("Direct message received - calling handlers", {
3139
+ threadId,
3140
+ handlerCount: this.directMessageHandlers.length
3141
+ });
3142
+ const channel = thread.channel;
3143
+ for (const handler of this.directMessageHandlers) {
3144
+ await handler(thread, message, channel, context);
3145
+ }
3146
+ return;
3147
+ }
3148
+ if (isDM) {
3149
+ message.isMention = true;
3150
+ }
3151
+ if (isSubscribed) {
3152
+ this.logger.debug("Message in subscribed thread - calling handlers", {
3153
+ threadId,
3154
+ handlerCount: this.subscribedMessageHandlers.length
3155
+ });
3156
+ await this.runHandlers(
3157
+ this.subscribedMessageHandlers,
3158
+ thread,
3159
+ message,
3160
+ context
3161
+ );
3162
+ return;
3163
+ }
3164
+ if (message.isMention) {
3165
+ this.logger.debug("Bot mentioned", {
3166
+ threadId,
3167
+ text: message.text.slice(0, 100)
3168
+ });
3169
+ await this.runHandlers(this.mentionHandlers, thread, message, context);
3170
+ return;
3171
+ }
3172
+ this.logger.debug("Checking message patterns", {
3173
+ patternCount: this.messagePatterns.length,
3174
+ patterns: this.messagePatterns.map((p) => p.pattern.toString()),
3175
+ messageText: message.text
3176
+ });
3177
+ let matchedPattern = false;
3178
+ for (const { pattern, handler } of this.messagePatterns) {
3179
+ const matches = pattern.test(message.text);
3180
+ this.logger.debug("Pattern test", {
3181
+ pattern: pattern.toString(),
3182
+ text: message.text,
3183
+ matches
3184
+ });
3185
+ if (matches) {
3186
+ this.logger.debug("Message matched pattern - calling handler", {
3187
+ pattern: pattern.toString()
2916
3188
  });
3189
+ matchedPattern = true;
3190
+ await handler(thread, message, context);
2917
3191
  }
2918
- } finally {
2919
- await this._stateAdapter.releaseLock(lock);
2920
- this.logger.debug("Lock released", { threadId });
3192
+ }
3193
+ if (!matchedPattern) {
3194
+ this.logger.debug("No handlers matched message", {
3195
+ threadId,
3196
+ text: message.text.slice(0, 100)
3197
+ });
2921
3198
  }
2922
3199
  }
2923
3200
  createThread(adapter, threadId, initialMessage, isSubscribedContext = false) {
2924
3201
  const parts = threadId.split(":");
2925
3202
  const channelId = parts[1] || "";
2926
3203
  const isDM = adapter.isDM?.(threadId) ?? false;
3204
+ const channelVisibility = adapter.getChannelVisibility?.(threadId) ?? "unknown";
2927
3205
  return new ThreadImpl({
2928
3206
  id: threadId,
2929
3207
  adapter,
@@ -2932,6 +3210,7 @@ var Chat = class {
2932
3210
  initialMessage,
2933
3211
  isSubscribedContext,
2934
3212
  isDM,
3213
+ channelVisibility,
2935
3214
  currentMessage: initialMessage,
2936
3215
  logger: this.logger,
2937
3216
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
@@ -2974,9 +3253,45 @@ var Chat = class {
2974
3253
  escapeRegex(str) {
2975
3254
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2976
3255
  }
2977
- async runHandlers(handlers, thread, message) {
3256
+ /**
3257
+ * Reconstruct a proper Message instance from a dequeued entry.
3258
+ * After JSON roundtrip through the state adapter, the message is a plain
3259
+ * object (not a Message instance). This restores class invariants like
3260
+ * `links` defaulting to `[]` and `metadata.dateSent` being a Date.
3261
+ */
3262
+ rehydrateMessage(raw) {
3263
+ if (raw instanceof Message) {
3264
+ return raw;
3265
+ }
3266
+ const obj = raw;
3267
+ if (obj._type === "chat:Message") {
3268
+ return Message.fromJSON(obj);
3269
+ }
3270
+ const metadata = obj.metadata;
3271
+ const dateSent = metadata.dateSent;
3272
+ const editedAt = metadata.editedAt;
3273
+ return new Message({
3274
+ id: obj.id,
3275
+ threadId: obj.threadId,
3276
+ text: obj.text,
3277
+ formatted: obj.formatted,
3278
+ raw: obj.raw,
3279
+ author: obj.author,
3280
+ metadata: {
3281
+ dateSent: dateSent instanceof Date ? dateSent : new Date(dateSent),
3282
+ edited: metadata.edited,
3283
+ editedAt: editedAt ? new Date(
3284
+ editedAt instanceof Date ? editedAt.toISOString() : editedAt
3285
+ ) : void 0
3286
+ },
3287
+ attachments: obj.attachments ?? [],
3288
+ isMention: obj.isMention,
3289
+ links: obj.links ?? []
3290
+ });
3291
+ }
3292
+ async runHandlers(handlers, thread, message, context) {
2978
3293
  for (const handler of handlers) {
2979
- await handler(thread, message);
3294
+ await handler(thread, message, context);
2980
3295
  }
2981
3296
  }
2982
3297
  };