{"record":{"id":"2ab16710c610b932","repo":"paperclipai/paperclip","slug":"telegram-durable-draft-transport-is-unavailable","errorCode":null,"errorMessage":"Telegram durable draft transport is unavailable","messagePattern":"Telegram durable draft transport is unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-sdk-runtime.ts","lineNumber":2467,"sourceCode":"  async streamTelegramDraft(\n    threadId: string,\n    textStream: AsyncIterable<string>,\n    control: TelegramDraftControl,\n  ): Promise<{ id: string } | TelegramDraftStopped> {\n    const adapter = this.adapter as unknown as {\n      paperclipDraftStopVersion?: number;\n      stream(\n        threadId: string,\n        stream: AsyncIterable<string>,\n        options: unknown,\n      ): Promise<{ id: string } | TelegramDraftStopped>;\n    };\n    if (\n      this.provider !== \"telegram\" ||\n      !telegramPrivateDraftDestination(threadId) ||\n      adapter.paperclipDraftStopVersion !== 1\n    ) {\n      throw new Error(\"Telegram durable draft transport is unavailable\");\n    }\n    return adapter.stream(threadId, textStream, {\n      paperclipDraftControl: control,\n    });\n  }\n\n  /**\n   * Post one Slack file-only publication while durably recording the accepted\n   * upload IDs before the adapter performs its eventually-consistent share\n   * lookup. This specialized receipt scope is deliberately unavailable for\n   * cards, edits, and ordinary text sends; the send still uses Thread.post.\n   */\n  async postSlackFilePublication(\n    threadId: string,\n    message: Parameters<Thread[\"post\"]>[0],\n    onUploadAccepted: (\n      receipt: SlackFileUploadAcceptedReceipt,\n    ) => Promise<void>,","sourceCodeStart":2449,"sourceCodeEnd":2485,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-sdk-runtime.ts#L2449-L2485","documentation":"streamTelegramDraft only forwards a text stream to the Telegram adapter when three invariants hold: the runtime provider is 'telegram', the thread resolves to a private-chat draft destination, and the adapter declares paperclipDraftStopVersion === 1 (the durable stop protocol). If any check fails, the runtime refuses to start a draft stream it cannot durably control, so it throws this plain Error instead of silently degrading to a non-durable transport.","triggerScenarios":"Calling streamTelegramDraft(threadId, textStream, control) when: (1) the runtime was constructed for a provider other than 'telegram'; (2) telegramPrivateDraftDestination(threadId) returns falsy, i.e. the thread id is not a private-chat draft-capable destination (e.g. a group/channel thread); or (3) the wired Telegram adapter does not export paperclipDraftStopVersion === 1 (stale or third-party adapter build).","commonSituations":"A deployment where the chat endpoint config was switched to a different provider but old code paths still call the Telegram-only streaming API; a Telegram group/channel thread id passed in instead of a private chat; running an older Telegram adapter (or stubbed adapter in tests) that predates the draft-stop v1 contract.","solutions":["Verify the endpoint runtime is bound to the telegram provider before calling streamTelegramDraft (check this.provider / endpoint config).","Confirm the threadId denotes a private chat by running telegramPrivateDraftDestination(threadId) first; use the ordinary Thread.post path for group/channel threads.","Upgrade or re-wire the Telegram adapter so it exposes paperclipDraftStopVersion === 1, or feature-detect it and fall back to a plain post.","Wrap the call in try/catch and fall back to non-durable sending when the transport is unavailable."],"exampleFix":"// before\nawait runtime.streamTelegramDraft(threadId, textStream, control);\n\n// after\nif (\n  runtime.provider === \"telegram\" &&\n  telegramPrivateDraftDestination(threadId) &&\n  (runtime.getProviderAdapter() as { paperclipDraftStopVersion?: number })\n      .paperclipDraftStopVersion === 1\n) {\n  await runtime.streamTelegramDraft(threadId, textStream, control);\n} else {\n  await runtime.thread(threadId).post(text);\n}","handlingStrategy":"fallback","validationCode":"function canStreamTelegramDraft(runtime: unknown, threadId: string): boolean {\n  const r = runtime as { provider?: string; getProviderAdapter?: () => unknown };\n  const adapter = r.getProviderAdapter?.() as { paperclipDraftStopVersion?: number } | undefined;\n  return r.provider === \"telegram\" && telegramPrivateDraftDestination(threadId) && adapter?.paperclipDraftStopVersion === 1;\n}","typeGuard":"function isDraftCapableTelegramAdapter(a: unknown): a is { paperclipDraftStopVersion: 1; stream: Function } {\n  return typeof a === \"object\" && a !== null && (a as any).paperclipDraftStopVersion === 1 && typeof (a as any).stream === \"function\";\n}","tryCatchPattern":"try {\n  await runtime.streamTelegramDraft(threadId, textStream, control);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"durable draft transport is unavailable\")) {\n    await runtime.thread(threadId).post(text);\n  } else throw err;\n}","preventionTips":["Always gate Telegram draft streaming behind a provider + telegramPrivateDraftDestination check.","Feature-detect paperclipDraftStopVersion before using draft streaming.","Keep the Telegram adapter pinned to a version supporting draft-stop v1 in production deployments."],"tags":["telegram","adapter-compatibility","draft-streaming","provider-guard"],"backgroundTag":"unsupported-operation","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}