paperclipai/paperclip · error · TeamsAdapterCompatibilityError

openDM is unavailable

Error message

openDM is unavailable

What it means

Same compatibility gate as error 537: after verifying decodeThreadId, the runtime checks that adapter.openDM is a function and throws TeamsAdapterCompatibilityError('openDM is unavailable') when it is not. The adapter therefore cannot open direct-message threads and is rejected.

Source

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

 * outbound call to a fresh API client rooted at that route. Legacy thread ids
 * that embedded a URL remain readable, but a newer persisted route wins. A
 * context-local getter keeps simultaneous conversations isolated without
 * forcing unrelated Teams threads through a single network queue.
 */
export function scopeMicrosoftTeamsEgress(
  adapter: Adapter,
  configuredApiUrl?: string,
  enableFileConsent = false,
): Adapter {
  const teams = adapter as unknown as TeamsAdapterInternals;
  if (!teams.app?.api) {
    throw new TeamsAdapterCompatibilityError("app.api is unavailable");
  }
  if (typeof teams.decodeThreadId !== "function") {
    throw new TeamsAdapterCompatibilityError("decodeThreadId is unavailable");
  }
  if (typeof teams.openDM !== "function") {
    throw new TeamsAdapterCompatibilityError("openDM is unavailable");
  }
  if (typeof teams.cacheUserContext !== "function") {
    throw new TeamsAdapterCompatibilityError("cacheUserContext is unavailable");
  }
  if (typeof teams.getIncomingUser !== "function") {
    throw new TeamsAdapterCompatibilityError("getIncomingUser is unavailable");
  }
  if (typeof teams.getUser !== "function") {
    throw new TeamsAdapterCompatibilityError("getUser is unavailable");
  }
  for (const methodName of TEAMS_THREAD_SCOPED_METHODS) {
    if (typeof teams[methodName] !== "function") {
      throw new TeamsAdapterCompatibilityError(`${methodName} is unavailable`);
    }
  }
  if (
    typeof teams.app.api.constructor !== "function" ||
    !("http" in teams.app.api)

View on GitHub (pinned to 01ad858492)

Solutions

  1. Update the Teams adapter package to the version matching the runtime's expected internals.
  2. Add/implement openDM on a custom adapter per the runtime contract.
  3. Extend the mock with openDM: vi.fn().
  4. Confirm no wrapper/proxy around the adapter deletes or renames openDM.

Example fix

// before
const adapter = { app: { api: {} }, decodeThreadId: vi.fn() };
// after
const adapter = { app: { api: {} }, decodeThreadId: vi.fn(), openDM: vi.fn() };
Defensive patterns

Strategy: type-guard

Validate before calling

const teams = adapter as unknown as TeamsAdapterInternals;
if (typeof teams.openDM !== "function") throw new Error("adapter missing openDM");

Type guard

function supportsOpenDM(a: unknown): a is TeamsAdapterInternals {
  return typeof (a as TeamsAdapterInternals)?.openDM === "function";
}

Try / catch

try {
  wireTeamsAdapter(adapter);
} catch (e) {
  if (e instanceof TeamsAdapterCompatibilityError && e.message.includes("openDM")) {
    // implement or upgrade so openDM exists
  }
}

Prevention

When it happens

Trigger: Wiring a Teams adapter whose internals include app.api and decodeThreadId but whose openDM property is missing or not a function (older adapter version, incomplete custom adapter, partial mock).

Common situations: Adapter package drift after a dependency update; a custom adapter implemented before openDM was added to the contract; mocks that stub only the methods a specific test touched.

Related errors


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