paperclipai/paperclip · warning · CommentSubmissionUnknownError

We couldn’t confirm whether this comment was saved. Review…

Error message

We couldn’t confirm whether this comment was saved. Review the conversation before starting another draft.

What it means

confirmedCommentResponse wraps comment-submission promises; if the response doesn't pass the validity check (e.g. no comment object in the payload), it throws CommentSubmissionUnknownError, whose message warns the user the save cannot be confirmed. This is the 'lost receipt' path: the request may have committed server-side but the response was unusable.

Solutions

  1. Open the issue conversation and visually verify whether the comment actually saved before re-submitting (the message's instruction) to avoid duplicates.
  2. Log the raw response body when valid() fails to identify the shape mismatch.
  3. Reconcile client state with a follow-up GET of the issue's comments instead of resubmitting blindly.
  4. Fix the server response contract if valid()'s predicate no longer matches the payload.

Example fix

// before
if (!valid(response)) throw new CommentSubmissionUnknownError();
// after
if (!valid(response)) {
  console.warn("comment response failed validity check", response);
  throw new CommentSubmissionUnknownError();
}
Defensive patterns

Strategy: fallback

Validate before calling

// after an unknown-outcome comment submit, reconcile before retrying
const comments = await issuesApi.getComments(issueId);
const saved = comments.some(c => c.clientId === pendingClientId);

Type guard

function isUnknownCommentOutcome(e: unknown): e is CommentSubmissionUnknownError {
  return e instanceof CommentSubmissionUnknownError;
}

Try / catch

try {
  await submitComment(payload);
} catch (e) {
  if (isUnknownCommentOutcome(e)) {
    const saved = await issuesApi.getComments(issueId).catch(() => []);
    if (!saved.some(c => c.clientId === payload.clientId)) {
      showResubmitPrompt(payload); // safe to re-enter, nothing was saved
    }
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A comment POST completes with an OK response whose body fails the valid() predicate — e.g. the update response contains no comment field, or a transformed payload is missing the expected issue/comment shape.

Common situations: Network proxy stripped or truncated the response body after the write committed; server returned 200 with an unexpected payload shape after a partial failure; serialization drift between server and UI after an upgrade.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/3d6310ca00635d93. Report an issue: GitHub.

Appendix: source

Thrown at ui/src/api/issues.ts:60

  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const row = value as Record<string, unknown>;
  const uuid = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
  return (
    typeof row.id === "string" &&
    uuid.test(row.id) &&
    typeof row.issueId === "string" &&
    uuid.test(row.issueId) &&
    typeof row.body === "string"
  );
}

async function confirmedCommentResponse<T>(
  request: Promise<T>,
  valid: (response: T) => boolean,
): Promise<T> {
  try {
    const response = await request;
    if (!valid(response)) throw new CommentSubmissionUnknownError();
    return response;
  } catch (error) {
    // A rejected request is distinct from a lost/invalid receipt. A server 5xx
    // may also happen after commit; it cannot establish that nothing was saved.
    if (error instanceof ApiError && error.status < 500) throw error;
    throw new CommentSubmissionUnknownError();
  }
}

export type IssueUpdateResponse = Issue & {
  comment?: IssueComment | null;
  changes: IssueChanges;
  blockedByIssueIds?: string[];
};

export type ResolveRecoveryActionResponse = {
  issue: Issue;
  recoveryAction: IssueRecoveryAction;

View on GitHub (pinned to 3f1d897a7c)