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
- Retry the request after the scope change settles; the new call will bind to the current scope and return fresh handles/cursors.
- Avoid switching project/thread while knowledge queries are in flight, or serialize inspector calls with session state changes.
- 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
- Block project/thread switching while knowledge requests are in flight (disable switcher during queries).
- Treat 'stale-handle' from any inspector call as retryable, not fatal.
- Keep inspector calls short-lived; paginate rather than issuing one giant request.
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
- GitHub token refresh no longer matches the active Factory wo
- Factory kickoff was queued onto an ending run and never reac
- Factory workspace GitHub credential registration is no longe
- Factory session ${session.sessionId} was retired during work
- Failed to fetch connection string — ${await extractError(res
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/247735748e0ff0c2.
Report an issue: GitHub.