mastra-ai/mastra · error

Message ${cursor} has no visible content (it may be an inter

Error message

Message ${cursor} has no visible content (it may be an internal system message). Try a neighboring message ID instead.

What it means

After resolving the cursor message, the reader formats its visible parts; if the message yields zero visible parts (content is a bare string with no parts, or all parts are internal `data-*` parts such as tool/internal metadata), it throws this error directing the caller to a neighboring message. Internal system messages are stored but not meant to be surfaced as recall anchors.

Source

Thrown at packages/memory/src/tools/om-tools.ts:811

  if (!threadId) {
    throw new Error('Thread ID is required for recall');
  }

  const resolved = await resolveCursorMessage(memory, cursor, {
    resourceId,
    threadScope,
    enforceThreadScope: false,
  });

  if ('hint' in resolved) {
    throw new Error(resolved.hint);
  }

  const allParts = formatMessageParts(resolved, 'high');

  if (allParts.length === 0) {
    throw new Error(
      `Message ${cursor} has no visible content (it may be an internal system message). Try a neighboring message ID instead.`,
    );
  }

  const target = [...allParts].reverse().find(p => p.partIndex === partIndex);

  if (!target) {
    const availableIndices = allParts.map(p => p.partIndex).join(', ');
    const highestVisiblePartIndex = Math.max(...allParts.map(p => p.partIndex));

    if (partIndex > highestVisiblePartIndex) {
      const nextMessage = await getNextVisibleMessage({
        memory,
        threadId,
        resourceId,
        after: resolved.createdAt,
      });

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Choose a message ID that appeared in visible recall output (user/assistant text) instead of one from raw storage.
  2. Use the previous or next message in the thread as the cursor, as the error suggests.
  3. List the thread with recall detail="low" to get only visible message IDs, then pick one.
  4. If this happens persistently, inspect the message content shape — a schema/migration may have stripped parts.

Example fix

// before
const cursor = allMessageIds[0]; // may be an internal system message
await recallDetail({ memory, threadId, cursor, partIndex: 0 });
// after
const visible = JSON.parse((await recallMessages({ memory, threadId, page: 1 })).messages);
const cursor = visible[0].messageId;
await recallDetail({ memory, threadId, cursor, partIndex: 0 });
Defensive patterns

Strategy: fallback

Try / catch

try {
  return await recallDetail({ memory, threadId, cursor, partIndex });
} catch (e) {
  if (e instanceof Error && e.message.includes('has no visible content')) {
    // step to a neighboring message and retry once
    const page = await recallMessages({ memory, threadId, page: 1, limit: 50 });
    const msgs = JSON.parse(page.messages);
    const idx = msgs.findIndex(m => m.messageId === cursor);
    const neighbor = msgs[idx + 1] ?? msgs[idx - 1];
    if (neighbor) return recallDetail({ memory, threadId, cursor: neighbor.messageId, partIndex });
  }
  throw e;
}

Prevention

When it happens

Trigger: Using a cursor that points to a system/internal message (e.g. a data-part-only message, an empty string-content message, or a tool-internal record) as the recall anchor; picking the last ID from storage metadata rather than from formatted recall output.

Common situations: Model picked a messageId that appeared in logs/storage but was filtered from visible recall output; threads seeded with system messages where index 0 is internal; cursors persisted from an older schema whose messages now format as empty.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/fae659e6dee3134a. Report an issue: GitHub.