mastra-ai/mastra · error

Knowledge curation cursor cannot move backwards

Error message

Knowledge curation cursor cannot move backwards

What it means

advanceCurationCursor() persists a per (sourceThreadId, agent) cursor over knowledge record IDs. Because cursors track progress through an ordered ID stream (ULIDs sort lexicographically by time), moving the cursor backwards would re-process or rewind curation work, so the method throws if input.lastKnowledgeId is lexicographically less than the stored cursor's ID.

Source

Thrown at packages/core/src/storage/domains/knowledge/inmemory.ts:512

      });
    }
    return results.slice(0, input.limit ?? 20);
  }

  async getCurationCursor(input: { sourceThreadId: string; agent: string }): Promise<KnowledgeCurationCursor | null> {
    const cursor = this.#db.knowledgeCursors.get(`${input.sourceThreadId}\u0000${input.agent}`);
    return cursor ? { ...cursor, updatedAt: new Date(cursor.updatedAt) } : null;
  }

  async advanceCurationCursor(input: {
    sourceThreadId: string;
    agent: string;
    lastKnowledgeId: string;
  }): Promise<KnowledgeCurationCursor> {
    const key = `${input.sourceThreadId}\u0000${input.agent}`;
    const existing = this.#db.knowledgeCursors.get(key);
    if (existing && input.lastKnowledgeId < existing.lastKnowledgeId)
      throw new Error('Knowledge curation cursor cannot move backwards');
    const cursor = { ...input, updatedAt: new Date() };
    this.#db.knowledgeCursors.set(key, cursor);
    return { ...cursor };
  }

  async listActivity(input: {
    scope: KnowledgeScope;
    after?: string;
    limit?: number;
  }): Promise<KnowledgeActivityEvent[]> {
    const queryScope = canonicalizeKnowledgeScope(input.scope);
    return this.#db.knowledgeActivity
      .filter(event => isKnowledgeScopeVisible(event.scope, queryScope))
      .filter(event => !input.after || event.id < input.after)
      .sort((a, b) => b.id.localeCompare(a.id))
      .slice(0, input.limit ?? 100)
      .map(event => ({ ...event, scope: [...event.scope], createdAt: new Date(event.createdAt) }));
  }

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Only advance cursors from records listed after the current cursor position (use listKnowledge pagination).
  2. Load the existing cursor first and skip the update if input.lastKnowledgeId <= existing.lastKnowledgeId.
  3. Ensure a single writer per (sourceThreadId, agent) or serialize cursor updates.
  4. If a genuine rewind is needed, delete/rekey the cursor rather than advancing it backwards.

Example fix

// before
await storage.advanceCurationCursor({ sourceThreadId, agent, lastKnowledgeId: batch.first.id });
// after
const latest = batch.records.reduce((a, r) => (r.id > a ? r.id : a), batch.records[0]?.id);
await storage.advanceCurationCursor({ sourceThreadId, agent, lastKnowledgeId: latest });
Defensive patterns

Strategy: validation

Validate before calling

const prev = cursors.get(`${sourceThreadId}\u0000${agent}`);
if (prev && nextLastKnowledgeId <= prev.lastKnowledgeId) return; // no-op, already advanced

Try / catch

try {
  await storage.advanceCurationCursor(input);
} catch (e) {
  if (e.message.includes('cannot move backwards')) return; // stale writer; ignore
  throw e;
}

Prevention

When it happens

Trigger: Calling advanceCurationCursor() with lastKnowledgeId older than the currently stored cursor for the same sourceThreadId+agent pair — e.g., re-running an old batch, resuming from a stale checkpoint, or iterating records in non-ULID order.

Common situations: Two workers curating the same thread concurrently and the slower one writing an older cursor; replaying an event log from the beginning; restoring checkpoints from a backup while the store advanced.

Related errors


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