paperclipai/paperclip · error

Teams upload is not confirmed

Error message

Teams upload is not confirmed

What it means

fileInfo() builds the Teams 'file info' attachment card (contentType application/vnd.microsoft.teams.card.file.info) that is sent after the OneDrive upload completes. It refuses to run unless the capability's #confirmed flag is true, i.e. a previous exchange('put') classified the upload as uploaded. This prevents sending a download link for a file that was never actually committed to the recipient's OneDrive.

Source

Thrown at server/src/services/chat-teams-file-consent.ts:414

      .parse(await openPrivate(context, "upload", material));
    if (
      value.bindingDigest !== digest(binding) ||
      value.info.name !== binding.filename ||
      typeof sharePointUrl(value.info.uploadUrl) === "string" ||
      typeof sharePointUrl(value.info.contentUrl, true) === "string" ||
      (value.confirmed && !value.putStarted)
    )
      throw new Error("Invalid Teams upload state");
    const result = new UploadCapability(value.info, binding);
    result.#confirmed = value.confirmed;
    result.#putStarted = value.putStarted;
    return result;
  }
  matches(binding: TeamsFileConsentBinding): boolean {
    return this.#bindingDigest === digest(binding);
  }
  fileInfo() {
    if (!this.#confirmed) throw new Error("Teams upload is not confirmed");
    return {
      contentType: "application/vnd.microsoft.teams.card.file.info" as const,
      name: this.#info.name,
      contentUrl: this.#info.contentUrl,
      content: { uniqueId: this.#info.uniqueId, fileType: this.#info.fileType },
    };
  }
  async exchange(
    operation: "put" | "status",
    bytes: Buffer | null,
    options: UploadRequestOptions,
  ): Promise<TeamsUploadOutcome> {
    if (
      (operation !== "put" && operation !== "status") ||
      options.byteSize !== this.#byteSize
    )
      throw new Error("Invalid Teams upload binding");
    // The capability is itself a security boundary; calling it directly cannot

View on GitHub (pinned to 01ad858492)

Solutions

  1. Only call fileInfo()/buildTeamsUploadedFileCard after exchange('put') returned an outcome with kind === 'uploaded'.
  2. After restoring from sealed state, check the confirmed flag first (via outcome of a status operation or by gating on the persisted phase reaching file_info_pending) before building the card.
  3. If the upload is incomplete, re-drive the state machine: exchange('put') or exchange('status') until kind === 'uploaded', advancing phases via nextTeamsFileConsentPhase.
  4. If the session expired (session_unavailable), restart the whole consent flow with a fresh binding rather than fabricating a file card.

Example fix

// before
const card = buildTeamsUploadedFileCard(upload, outcome); // throws if not uploaded

// after
if (outcome.kind !== "uploaded") {
  throw new Error(`upload not complete: ${outcome.kind}${"reason" in outcome ? "/" + outcome.reason : ""}`);
}
const card = buildTeamsUploadedFileCard(upload, outcome);
Defensive patterns

Strategy: type-guard

Validate before calling

const outcome = await exchangeTeamsFileUpload({ upload, binding, operation: "status" });
if (outcome.kind !== "uploaded") throw new Error("upload not confirmed; cannot send file card");

Type guard

function isConfirmedOutcome(outcome: TeamsUploadOutcome): outcome is Extract<TeamsUploadOutcome, { kind: "uploaded" }> {
  return outcome.kind === "uploaded";
}

Try / catch

try {
  const card = buildTeamsUploadedFileCard(upload, outcome);
} catch (e) {
  if (e instanceof Error && e.message === "Teams upload is not confirmed") {
    // re-drive exchange("put")/exchange("status") until uploaded, or restart flow
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling fileInfo() (directly or via buildTeamsUploadedFileCard) on an UploadCapability that was restored with confirmed=false, or on a freshly created capability whose exchange('put') has not yet returned { kind: 'uploaded' }, or whose PUT ended uncertain/incomplete.

Common situations: Worker resumes after restart and builds the file card before re-running the upload; caller ignores the exchange() outcome (uncertain/incomplete) and proceeds to send the card; the upload session expired and returned session_unavailable but downstream code treats the flow as done.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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