chat 4.18.0 → 4.19.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/README.md CHANGED
@@ -40,6 +40,8 @@ bot.onSubscribedMessage(async (thread, message) => {
40
40
  });
41
41
  ```
42
42
 
43
+ > **Tip:** PostgreSQL and ioredis adapters are also available for production. See [State Adapters](https://chat-sdk.dev/docs/state) for all options.
44
+
43
45
  ## Configuration
44
46
 
45
47
  | Option | Type | Default | Description |
package/dist/index.d.ts CHANGED
@@ -77,6 +77,18 @@ interface ChatConfig<TAdapters extends Record<string, Adapter> = Record<string,
77
77
  * Pass "silent" to disable all logging.
78
78
  */
79
79
  logger?: Logger | LogLevel;
80
+ /**
81
+ * Behavior when a thread lock cannot be acquired (another handler is processing).
82
+ * - `'drop'` (default) — throw `LockError`, preserving current behavior
83
+ * - `'force'` — force-release the existing lock and re-acquire
84
+ * - callback — custom logic receiving `(threadId, message)`, return `'force'` or `'drop'`
85
+ *
86
+ * When `'force'` is used, the previous handler continues executing — only the lock
87
+ * is released, not the handler itself. This means two handlers may run concurrently
88
+ * on the same thread. The old handler's `releaseLock()` call becomes a no-op since
89
+ * the token no longer matches.
90
+ */
91
+ onLockConflict?: "force" | "drop" | ((threadId: string, message: Message) => "force" | "drop" | Promise<"force" | "drop">);
80
92
  /** State adapter for subscriptions and locking */
81
93
  state: StateAdapter;
82
94
  /**
@@ -122,7 +134,7 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
122
134
  * Default fallback: first two colon-separated parts (e.g., "slack:C123").
123
135
  * Adapters with different structures should override this.
124
136
  */
125
- channelIdFromThreadId?(threadId: string): string;
137
+ channelIdFromThreadId(threadId: string): string;
126
138
  /** Decode thread ID string back to platform-specific data */
127
139
  decodeThreadId(threadId: string): TThreadId;
128
140
  /** Delete a message */
@@ -248,13 +260,27 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
248
260
  * @param message - The message content
249
261
  * @returns EphemeralMessage with usedFallback: false
250
262
  */
251
- postEphemeral?(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
263
+ postEphemeral?(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage<TRawMessage>>;
252
264
  /** Post a message to a thread */
253
265
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>;
254
266
  /** Remove a reaction from a message */
255
267
  removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
256
268
  /** Render formatted content to platform-specific string */
257
269
  renderFormatted(content: FormattedContent): string;
270
+ /**
271
+ * Schedule a message for future delivery.
272
+ *
273
+ * Optional — only supported by adapters with native scheduling APIs (e.g., Slack).
274
+ * Thread.schedule() will throw NotImplementedError if this method is absent.
275
+ *
276
+ * @param threadId - The thread to post in
277
+ * @param message - The message content
278
+ * @param options - Scheduling options including the target delivery time
279
+ * @returns A ScheduledMessage with cancel() capability
280
+ */
281
+ scheduleMessage?(threadId: string, message: AdapterPostableMessage, options: {
282
+ postAt: Date;
283
+ }): Promise<ScheduledMessage<TRawMessage>>;
258
284
  /** Show typing indicator */
259
285
  startTyping(threadId: string, status?: string): Promise<void>;
260
286
  /**
@@ -406,6 +432,12 @@ interface StateAdapter {
406
432
  disconnect(): Promise<void>;
407
433
  /** Extend a lock's TTL */
408
434
  extendLock(lock: Lock, ttlMs: number): Promise<boolean>;
435
+ /**
436
+ * Force-release a lock on a thread, regardless of ownership token.
437
+ * The previous lock holder's handler continues running — only the lock is released.
438
+ * The old handler's `releaseLock()` becomes a no-op (token mismatch).
439
+ */
440
+ forceReleaseLock(threadId: string): Promise<void>;
409
441
  /** Get a cached value by key */
410
442
  get<T = unknown>(key: string): Promise<T | null>;
411
443
  /** Check if subscribed to a thread */
@@ -456,7 +488,30 @@ interface Postable<TState = Record<string, unknown>, TRawMessage = unknown> {
456
488
  /**
457
489
  * Post an ephemeral message visible only to a specific user.
458
490
  */
459
- postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
491
+ postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage<TRawMessage> | null>;
492
+ /**
493
+ * Schedule a message for future delivery.
494
+ *
495
+ * Currently only supported by the Slack adapter. Other adapters
496
+ * will throw NotImplementedError.
497
+ *
498
+ * @param message - The message content (streaming not supported)
499
+ * @param options - Scheduling options including the target delivery time
500
+ * @returns A ScheduledMessage with cancel() capability
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * const scheduled = await thread.schedule("Reminder: standup!", {
505
+ * postAt: new Date("2026-03-09T09:00:00Z"),
506
+ * });
507
+ *
508
+ * // Cancel before it's sent
509
+ * await scheduled.cancel();
510
+ * ```
511
+ */
512
+ schedule(message: AdapterPostableMessage | ChatElement, options: {
513
+ postAt: Date;
514
+ }): Promise<ScheduledMessage<TRawMessage>>;
460
515
  /**
461
516
  * Set the state. Merges with existing state by default.
462
517
  */
@@ -630,7 +685,7 @@ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extend
630
685
  * }
631
686
  * ```
632
687
  */
633
- postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
688
+ postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage<TRawMessage> | null>;
634
689
  /** Recently fetched messages (cached) */
635
690
  recentMessages: Message<TRawMessage>[];
636
691
  /**
@@ -796,11 +851,11 @@ interface SentMessage<TRawMessage = unknown> extends Message<TRawMessage> {
796
851
  * Ephemeral messages are visible only to a specific user and typically
797
852
  * cannot be edited or deleted (platform-dependent).
798
853
  */
799
- interface EphemeralMessage {
854
+ interface EphemeralMessage<TRawMessage = unknown> {
800
855
  /** Message ID (may be empty for some platforms) */
801
856
  id: string;
802
857
  /** Platform-specific raw response */
803
- raw: unknown;
858
+ raw: TRawMessage;
804
859
  /** Thread ID where message was sent (or DM thread if fallback was used) */
805
860
  threadId: string;
806
861
  /** Whether this used native ephemeral or fell back to DM */
@@ -818,6 +873,24 @@ interface PostEphemeralOptions {
818
873
  */
819
874
  fallbackToDM: boolean;
820
875
  }
876
+ /**
877
+ * Result of scheduling a message for future delivery.
878
+ *
879
+ * Currently only supported by the Slack adapter via `chat.scheduleMessage`.
880
+ * Other adapters will throw `NotImplementedError` when `schedule()` is called.
881
+ */
882
+ interface ScheduledMessage<TRawMessage = unknown> {
883
+ /** Cancel the scheduled message before it's sent */
884
+ cancel(): Promise<void>;
885
+ /** Channel ID where the message will be posted */
886
+ channelId: string;
887
+ /** When the message will be sent */
888
+ postAt: Date;
889
+ /** Platform-specific raw response */
890
+ raw: TRawMessage;
891
+ /** Platform-specific scheduled message ID */
892
+ scheduledMessageId: string;
893
+ }
821
894
  /**
822
895
  * Input type for adapter postMessage/editMessage methods.
823
896
  * This excludes streams since adapters handle content synchronously.
@@ -1576,6 +1649,9 @@ declare class ChannelImpl<TState = Record<string, unknown>> implements Channel<T
1576
1649
  post(message: string | PostableMessage | ChatElement): Promise<SentMessage>;
1577
1650
  private postSingleMessage;
1578
1651
  postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
1652
+ schedule(message: AdapterPostableMessage | ChatElement, options: {
1653
+ postAt: Date;
1654
+ }): Promise<ScheduledMessage>;
1579
1655
  startTyping(status?: string): Promise<void>;
1580
1656
  mentionUser(userId: string): string;
1581
1657
  toJSON(): SerializedChannel;
@@ -1586,8 +1662,6 @@ declare class ChannelImpl<TState = Record<string, unknown>> implements Channel<T
1586
1662
  }
1587
1663
  /**
1588
1664
  * Derive the channel ID from a thread ID.
1589
- * Uses adapter.channelIdFromThreadId if available, otherwise defaults to
1590
- * first two colon-separated parts.
1591
1665
  */
1592
1666
  declare function deriveChannelId(adapter: Adapter, threadId: string): string;
1593
1667
 
@@ -1662,6 +1736,7 @@ declare class Chat<TAdapters extends Record<string, Adapter> = Record<string, Ad
1662
1736
  private readonly _streamingUpdateIntervalMs;
1663
1737
  private readonly _fallbackStreamingPlaceholderText;
1664
1738
  private readonly _dedupeTtlMs;
1739
+ private readonly _onLockConflict;
1665
1740
  private readonly mentionHandlers;
1666
1741
  private readonly messagePatterns;
1667
1742
  private readonly subscribedMessageHandlers;
@@ -2203,6 +2278,9 @@ declare class ThreadImpl<TState = Record<string, unknown>> implements Thread<TSt
2203
2278
  unsubscribe(): Promise<void>;
2204
2279
  post(message: string | PostableMessage | ChatElement): Promise<SentMessage>;
2205
2280
  postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
2281
+ schedule(message: AdapterPostableMessage | ChatElement, options: {
2282
+ postAt: Date;
2283
+ }): Promise<ScheduledMessage>;
2206
2284
  /**
2207
2285
  * Handle streaming from an AsyncIterable.
2208
2286
  * Normalizes the stream (supports both textStream and fullStream from AI SDK),
@@ -2697,4 +2775,4 @@ declare const Select: SelectComponent;
2697
2775
  declare const SelectOption: SelectOptionComponent;
2698
2776
  declare const TextInput: TextInputComponent;
2699
2777
 
2700
- export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageData, type MessageHandler, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableRaw, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toCardElement, toModalElement, toPlainText, walkAst };
2778
+ export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageData, type MessageHandler, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableRaw, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toCardElement, toModalElement, toPlainText, walkAst };
package/dist/index.js CHANGED
@@ -472,6 +472,25 @@ var ChannelImpl = class _ChannelImpl {
472
472
  }
473
473
  return null;
474
474
  }
475
+ async schedule(message, options) {
476
+ let postable;
477
+ if (isJSX(message)) {
478
+ const card = toCardElement(message);
479
+ if (!card) {
480
+ throw new Error("Invalid JSX element: must be a Card element");
481
+ }
482
+ postable = card;
483
+ } else {
484
+ postable = message;
485
+ }
486
+ if (!this.adapter.scheduleMessage) {
487
+ throw new NotImplementedError(
488
+ "Scheduled messages are not supported by this adapter",
489
+ "scheduling"
490
+ );
491
+ }
492
+ return this.adapter.scheduleMessage(this.id, postable, options);
493
+ }
475
494
  async startTyping(status) {
476
495
  await this.adapter.startTyping(this.id, status);
477
496
  }
@@ -555,11 +574,7 @@ var ChannelImpl = class _ChannelImpl {
555
574
  }
556
575
  };
557
576
  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(":");
577
+ return adapter.channelIdFromThreadId(threadId);
563
578
  }
564
579
  function extractMessageContent(message) {
565
580
  if (typeof message === "string") {
@@ -1114,6 +1129,25 @@ var ThreadImpl = class _ThreadImpl {
1114
1129
  }
1115
1130
  return null;
1116
1131
  }
1132
+ async schedule(message, options) {
1133
+ let postable;
1134
+ if (isJSX(message)) {
1135
+ const card = toCardElement(message);
1136
+ if (!card) {
1137
+ throw new Error("Invalid JSX element: must be a Card element");
1138
+ }
1139
+ postable = card;
1140
+ } else {
1141
+ postable = message;
1142
+ }
1143
+ if (!this.adapter.scheduleMessage) {
1144
+ throw new NotImplementedError(
1145
+ "Scheduled messages are not supported by this adapter",
1146
+ "scheduling"
1147
+ );
1148
+ }
1149
+ return this.adapter.scheduleMessage(this.id, postable, options);
1150
+ }
1117
1151
  /**
1118
1152
  * Handle streaming from an AsyncIterable.
1119
1153
  * Normalizes the stream (supports both textStream and fullStream from AI SDK),
@@ -1529,6 +1563,7 @@ var Chat = class {
1529
1563
  _streamingUpdateIntervalMs;
1530
1564
  _fallbackStreamingPlaceholderText;
1531
1565
  _dedupeTtlMs;
1566
+ _onLockConflict;
1532
1567
  mentionHandlers = [];
1533
1568
  messagePatterns = [];
1534
1569
  subscribedMessageHandlers = [];
@@ -1557,6 +1592,7 @@ var Chat = class {
1557
1592
  this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
1558
1593
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
1559
1594
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1595
+ this._onLockConflict = config.onLockConflict;
1560
1596
  if (typeof config.logger === "string") {
1561
1597
  this.logger = new ConsoleLogger(config.logger);
1562
1598
  } else {
@@ -2489,15 +2525,26 @@ var Chat = class {
2489
2525
  });
2490
2526
  return;
2491
2527
  }
2492
- const lock = await this._stateAdapter.acquireLock(
2528
+ let lock = await this._stateAdapter.acquireLock(
2493
2529
  threadId,
2494
2530
  DEFAULT_LOCK_TTL_MS
2495
2531
  );
2496
2532
  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
- );
2533
+ const resolution = typeof this._onLockConflict === "function" ? await this._onLockConflict(threadId, message) : this._onLockConflict ?? "drop";
2534
+ if (resolution === "force") {
2535
+ this.logger.info("Force-releasing lock on thread", { threadId });
2536
+ await this._stateAdapter.forceReleaseLock(threadId);
2537
+ lock = await this._stateAdapter.acquireLock(
2538
+ threadId,
2539
+ DEFAULT_LOCK_TTL_MS
2540
+ );
2541
+ }
2542
+ if (!lock) {
2543
+ this.logger.warn("Could not acquire lock on thread", { threadId });
2544
+ throw new LockError(
2545
+ `Could not acquire lock on thread ${threadId}. Another instance may be processing.`
2546
+ );
2547
+ }
2501
2548
  }
2502
2549
  this.logger.debug("Lock acquired", { threadId, token: lock.token });
2503
2550
  try {