paperclipai/paperclip · error · DiscordAdapterCompatibilityError

ensureRootThread is unavailable

Error message

ensureRootThread is unavailable

What it means

On a Discord endpoint, ensureDiscordRootThread still requires the adapter to expose an ensureRootThread(channelId, messageId, content) extension. If the wired Discord adapter does not define it, the runtime cannot create the root thread and throws a DiscordAdapterCompatibilityError rather than failing silently.

Source

Thrown at server/src/services/chat-sdk-runtime.ts:2631

  async ensureDiscordRootThread(input: {
    channelId: string;
    content: string;
    messageId: string;
  }): Promise<void> {
    if (this.provider !== "discord") {
      throw new DiscordAdapterCompatibilityError(
        "ensureDiscordRootThread was called for a non-Discord endpoint",
      );
    }
    const discord = this.adapter as unknown as DiscordAdapterInternals & {
      ensureRootThread?: (
        channelId: string,
        messageId: string,
        content: string,
      ) => Promise<unknown>;
    };
    if (typeof discord.ensureRootThread !== "function") {
      throw new DiscordAdapterCompatibilityError(
        "ensureRootThread is unavailable",
      );
    }
    await discord.ensureRootThread.call(
      this.adapter,
      input.channelId,
      input.messageId,
      input.content,
    );
  }

  /**
   * Reuse the pinned provider parser when a Telegram slash command also
   * carries media in its caption. The Chat SDK exposes slash-command fields
   * separately from the parsed Message, so without this bridge Paperclip
   * would silently discard a captioned document/photo/audio/video.
   */
  parseTelegramCommandMessage(raw: unknown): Message | null {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Upgrade the Discord adapter to a version implementing ensureRootThread.
  2. Feature-detect discord.ensureRootThread before calling and handle the missing capability (skip bootstrap or post to the channel directly).
  3. Update test doubles to include ensureRootThread.

Example fix

// before
await runtime.ensureDiscordRootThread({ channelId, content, messageId });

// after
const discord = runtime.getProviderAdapter() as { ensureRootThread?: unknown };
if (typeof discord.ensureRootThread === "function") {
  await runtime.ensureDiscordRootThread({ channelId, content, messageId });
} else {
  await runtime.channel(channelId).post(content);
}
Defensive patterns

Strategy: type-guard

Validate before calling

const discord = runtime.getProviderAdapter() as Record<string, unknown>;
if (typeof discord.ensureRootThread !== "function") throw new Error("discord adapter lacks ensureRootThread");

Type guard

function supportsRootThreads(a: unknown): a is { ensureRootThread: (channelId: string, messageId: string, content: string) => Promise<unknown> } {
  return typeof a === "object" && a !== null && typeof (a as any).ensureRootThread === "function";
}

Try / catch

try {
  await runtime.ensureDiscordRootThread({ channelId, content, messageId });
} catch (err) {
  if (err instanceof DiscordAdapterCompatibilityError && err.message.includes("ensureRootThread is unavailable")) {
    await runtime.channel(channelId).post(content);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling ensureDiscordRootThread on a Discord runtime whose adapter lacks ensureRootThread — e.g. an older Discord adapter build, a custom adapter, or a test double that only mocks message posting.

Common situations: Discord adapter version pinned before the root-thread support landed; hand-rolled Discord adapters; unit-test stubs missing the extension method.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/f91af8ca25a0d160. Report an issue: GitHub.