mastra-ai/mastra · error
resolveSession returned a session for resourceId="${session.
Error message
resolveSession returned a session for resourceId="${session.identity.getResourceId()}", but the mapped channel thread belongs to resourceId="${channelResourceId}". A session can only bind threads it owns — resolve the session under the thread's resourceId, or map the thread onto the session's resourceId with the channels `resolveResourceId` option. What it means
When a custom resolveSession is configured on channels, the session it returns must be bound to the same resourceId that the channel thread mapping resolved to. If session.identity.getResourceId() differs from the thread's channel resourceId, binding would violate ownership (a session may only bind threads it owns), so this explicit mismatch error is thrown instead of an opaque 'Thread not found' deeper in the stack.
Source
Thrown at packages/core/src/channels/agent-controller-channels.ts:432
// exists. It gets the same controller so this channels instance stays the
// owner of outbound delivery for whatever session it returns. A throw is
// tagged as a refusal so the channel error boundary keeps it out of the
// chat thread instead of posting the host's authorization message there.
const session = this.resolveSession
? await this.resolveChannelSession({ controller, thread, requestContext })
: await controller.createSession({
resourceId: channelResourceId,
id: channelResourceId,
ownerId: controller.id,
requestContext,
});
// A session may only bind threads it owns, and the mapped thread's owner is
// fixed by the channel's resourceId mapping. Catching the mismatch here
// turns an opaque "Thread not found" from deep inside the session into the
// actual instruction: line the two up, which hosts do by pairing this
// resolver with `resolveResourceId`.
if (this.resolveSession && session.identity.getResourceId() !== channelResourceId) {
throw new Error(
`resolveSession returned a session for resourceId="${session.identity.getResourceId()}", but the mapped channel thread belongs to resourceId="${channelResourceId}". A session can only bind threads it owns — resolve the session under the thread's resourceId, or map the thread onto the session's resourceId with the channels \`resolveResourceId\` option.`,
);
}
// Bind the mapped thread. Guard is mandatory: `switch` aborts any active
// run, so never re-switch when the session is already on this thread.
if (session.thread.getId() !== thread.id) {
await session.thread.switch({ threadId: thread.id });
}
await this.runSessionStartHook(session, thread, requestContext);
return session;
}
/**
* Call the host's resolver and tag anything it throws as a refusal. Covers
* synchronous throws too — the hook may return a `Session` directly, so a
* plain `.catch()` on the return value would miss them.
*/
private async resolveChannelSession(ctx: ChannelSessionResolveContext): Promise<Session<any>> {View on GitHub (pinned to 75dd419e61)
Solutions
- Configure the channel's `resolveResourceId` option so the thread maps onto the same resourceId the session resolver uses
- Change the custom resolveSession to resolve sessions under the thread's resourceId (channelResourceId)
- Unify resourceId derivation in one helper used by both the resolver and the channel mapping
Example fix
// before
resolveSession: async ({ memory }) => sessionFor(memory, userId), // resourceId=userId
// after
new AgentChannels({
resolveSession: async ({ memory }) => sessionFor(memory, channelResourceId),
resolveResourceId: ({ channelThreadId }) => channelResourceIdFor(channelThreadId),
}) Defensive patterns
Strategy: validation
Validate before calling
const session = await resolveSession(args);
const sessionResourceId = session.identity.getResourceId();
if (sessionResourceId !== channelResourceId) {
throw new Error(`resolveSession resourceId ${sessionResourceId} != channel resourceId ${channelResourceId}; align via resolveResourceId`);
} Type guard
function ownsThread(session: { identity: { getResourceId(): string } }, channelResourceId: string): boolean {
return session.identity.getResourceId() === channelResourceId;
} Try / catch
try {
const session = await channels.getSessionForThread(args);
} catch (err) {
if (err instanceof Error && err.message.includes('can only bind threads it owns')) {
logger.error({ sessionResourceId, channelResourceId }, 'resourceId mismatch in resolveSession');
} else throw err;
} Prevention
- Derive resourceId for sessions and channel threads from one shared helper
- Configure channels `resolveResourceId` when using custom resolveSession
- Write an integration test resolving a session for every mapped thread shape
When it happens
Trigger: getSessionForThread with this.resolveSession set, where the custom resolver creates/returns sessions under a resourceId (e.g. user ID) different from the thread's channel-resolved resourceId (e.g. platform-wide or channel ID), and resolveResourceId is not configured to align them.
Common situations: Custom session resolvers keyed on end-user identity while channel threads are mapped to a workspace/channel resourceId; migrating from default session resolution to a custom resolver without updating resourceId mapping.
Related errors
- MastraFactory: integrations [${channelRegistrations.map(({ i
- Channels require storage to be configured on the Mastra inst
- No adapter for platform "${platform}"
- Storage is required for tool approval lookups
- No channel context — cannot determine platform or thread
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/f2f232267d7fdb38.
Report an issue: GitHub.