{"record":{"id":"132005fc6558f8e2","repo":"paperclipai/paperclip","slug":"teams-file-cards-require-an-exact-personal-convers","errorCode":null,"errorMessage":"Teams file cards require an exact personal conversation","messagePattern":"Teams file cards require an exact personal conversation","errorType":"validation","errorClass":"TeamsServiceUrlValidationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-sdk-runtime.ts","lineNumber":1245,"sourceCode":"      const card =\n        kind === \"consent\"\n          ? parseTeamsFileConsentCard(input)\n          : kind === \"file_info\"\n            ? parseTeamsUploadedFileCard(input)\n            : null;\n      if (!card)\n        throw new TeamsAdapterCompatibilityError(\"invalid file-card shape\");\n      const decoded = teams.decodeThreadId!(threadId);\n      // Never infer personal scope from a missing type or conversation prefix.\n      if (\n        decoded.conversationType !== \"personal\" ||\n        typeof decoded.conversationId !== \"string\" ||\n        !decoded.conversationId ||\n        decoded.conversationId.length > 1024 ||\n        /[\\x00-\\x20\\x7f]/.test(decoded.conversationId) ||\n        /;messageid=/i.test(decoded.conversationId)\n      ) {\n        throw new TeamsServiceUrlValidationError(\n          \"Teams file cards require an exact personal conversation\",\n        );\n      }\n      return await withThreadServiceUrl(\n        threadId,\n        async () => {\n          let result: unknown;\n          try {\n            // Direct App attachment send is deliberately inside the same route\n            // ALS as ordinary thread operations. Thread.post would turn these\n            // provider-native cards into AdaptiveCards.\n            result = await teams.app!.send!(decoded.conversationId as string, {\n              type: \"message\",\n              attachments: [card],\n            });\n          } catch {\n            // Provider errors may contain private card contexts. No implicit\n            // retry: an uncertain POST remains caller-owned durable evidence.","sourceCodeStart":1227,"sourceCodeEnd":1263,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-sdk-runtime.ts#L1227-L1263","documentation":"Teams file cards can only be delivered inside a one-to-one personal conversation. When paperclipSendFileCard decodes the thread ID, it validates that the conversation type is 'personal' and that the conversation ID is a clean, bounded string (no control/whitespace chars, no ';messageid=' suffix). If any of these checks fail, this TeamsServiceUrlValidationError is thrown before any provider call is made.","triggerScenarios":"Calling teams.paperclipSendFileCard with a threadId whose decoded conversationType is not 'personal' (e.g. channel or groupChat), whose decoded.conversationId is empty/non-string, exceeds 1024 chars, contains control or whitespace characters, or embeds a ';messageid=' segment.","commonSituations":"Passing a channel thread ID where a DM thread ID is expected; using a conversationId copied from a message reference (which appends ;messageid=...); storing/reconstructing thread IDs in a lossy format that injects whitespace or control bytes.","solutions":["Verify the threadId refers to a personal (1:1) Teams conversation before sending a file card; decode it with teams.decodeThreadId and check conversationType === 'personal'.","Strip any ';messageid=...' suffix from the conversation ID and use the bare conversation ID.","Re-obtain the thread ID from an inbound personal-chat activity rather than deriving it from a channel or group message.","Sanitize the conversation ID: it must be non-empty, <=1024 chars, and contain no control characters (\\x00-\\x20, \\x7f)."],"exampleFix":"// before\nawait teams.paperclipSendFileCard(channelThreadId, \"consent\", input);\n// after\nconst decoded = teams.decodeThreadId(channelThreadId);\nif (decoded.conversationType !== \"personal\") {\n  throw new Error(\"File cards require a personal chat thread\");\n}\nawait teams.paperclipSendFileCard(channelThreadId, \"consent\", input);","handlingStrategy":"validation","validationCode":"const d = teams.decodeThreadId(threadId);\nconst ok = d.conversationType === \"personal\" && typeof d.conversationId === \"string\" &&\n  d.conversationId.length > 0 && d.conversationId.length <= 1024 &&\n  !/[\\x00-\\x20\\x7f]/.test(d.conversationId) && !/;messageid=/i.test(d.conversationId);\nif (!ok) throw new Error(\"refusing to send file card: not a clean personal conversation\");","typeGuard":"function isPersonalConversation(d: { conversationType?: string; conversationId?: unknown }): d is { conversationType: \"personal\"; conversationId: string } {\n  return d.conversationType === \"personal\" && typeof d.conversationId === \"string\" &&\n    d.conversationId.length > 0 && d.conversationId.length <= 1024 &&\n    !/[\\x00-\\x20\\x7f]/.test(d.conversationId) && !/;messageid=/i.test(d.conversationId);\n}","tryCatchPattern":"try {\n  await teams.paperclipSendFileCard(threadId, kind, input);\n} catch (err) {\n  if (err instanceof TeamsServiceUrlValidationError) {\n    logger.warn({ threadId }, \"file card skipped: thread is not a personal conversation\");\n    return; // skip card delivery, fall back to plain message\n  }\n  throw err;\n}","preventionTips":["Always derive threadId from activity.conversation.id of an inbound personal chat.","Strip ';messageid=' suffixes before using conversation IDs.","Never reuse channel/group thread IDs for file-card sends.","Validate decoded.conversationType before any card operation."],"tags":["teams","validation","conversation-id"],"backgroundTag":"invalid-argument-value","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"}