mastra-ai/mastra · error · KnowledgeInspectorError

stale-handle

stale-handle

Error message

Knowledge scope changed while the request was running.

What it means

The inspector stamps each request with an opaque identityKey and a fingerprint (owner\0resource\0thread). #assertStable() re-runs #binding() at the end of a request and throws 'stale-handle' if the fingerprint or identity key changed mid-request. This guarantees a response reflects one consistent scope even if the session's owner/project/thread changed while queries were running.

Source

Thrown at mastracode/sdk/src/knowledge-inspector.ts:490

      this.#cursors.clear();
    }
    return { ownerId, resourceId, threadId, fingerprint, identityKey: this.#identityKey };
  }

  #scope(binding: Binding, level: KnowledgeInspectorScopeLevel): KnowledgeScope {
    if (level === 'org') return [`org:${binding.ownerId}`];
    const scope = [`org:${binding.ownerId}`, `resource:${binding.resourceId}`];
    if (level === 'resource') return scope;
    if (!binding.threadId) {
      throw new KnowledgeInspectorError('unavailable', 'The active thread does not belong to this project.');
    }
    return [...scope, `thread:${binding.threadId}`];
  }

  async #assertStable(binding: Binding): Promise<void> {
    const current = await this.#binding();
    if (current.identityKey !== binding.identityKey || current.fingerprint !== binding.fingerprint) {
      throw new KnowledgeInspectorError('stale-handle', 'Knowledge scope changed while the request was running.');
    }
  }

  #assertVisible<T extends KnowledgeNode>(record: T | null, scope: KnowledgeScope): asserts record is T {
    if (!record || !isKnowledgeScopeVisible(record.scope, scope)) {
      throw new KnowledgeInspectorError('not-visible', 'Knowledge record is not visible in the selected scope.');
    }
  }

  #recordSummary(
    record: KnowledgeNode,
    binding: Binding,
    level: KnowledgeInspectorScopeLevel,
  ): KnowledgeInspectorNodeSummary {
    const type: KnowledgeInspectorRecordType = 'node';
    return {
      handle: this.#mintHandle(binding, level, type, record.id),
      type,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Retry the request after the scope change settles; the new call will bind to the current scope and return fresh handles/cursors.
  2. Avoid switching project/thread while knowledge queries are in flight, or serialize inspector calls with session state changes.
  3. Catch KnowledgeInspectorError code 'stale-handle' and surface 'scope changed' to the user, prompting a re-query.

Example fix

// before
const page = await inspector.listNodes({ level: 'resource' }); // may throw if user switches project mid-call
// after
try {
  const page = await inspector.listNodes({ level: 'resource' });
} catch (e) {
  if (e instanceof KnowledgeInspectorError && e.code === 'stale-handle') {
    return inspector.listNodes({ level: 'resource' }); // re-bind to current scope
  }
  throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// Validate scope stability before issuing long-running calls
const before = session.identity.getResourceId();
// ... after any await that may allow UI state change, re-check:
// if (session.identity.getResourceId() !== before) restart the request

Try / catch

async function withStableRetry<T>(fn: () => Promise<T>, retries = 2): Promise<T> {
  try {
    return await fn();
  } catch (e) {
    if (e instanceof KnowledgeInspectorError && e.code === 'stale-handle' && retries > 0) {
      return withStableRetry(fn, retries - 1); // re-binds to current scope
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: During an async inspector call (list/read/activity), the session's owner, project, or active thread changed — e.g. user switched project or thread — so #binding() regenerated the fingerprint and a new identityKey, invalidating the in-flight request's binding.

Common situations: Long-running knowledge queries racing a project switch in the TUI; a thread rotation completing while pagination fetches the next page; concurrent inspector calls while identity is refreshed.

Related errors


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