{"record":{"id":"7ef0006311deea37","repo":"mastra-ai/mastra","slug":"factory-agent-binding-is-unavailable-revoked-or","errorCode":null,"errorMessage":"Factory agent binding is unavailable, revoked, or no longer matches this session.","messagePattern":"Factory agent binding is unavailable, revoked, or no longer matches this session\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/tools.ts","lineNumber":83,"sourceCode":"        const currentResolution = await resolveFactorySessionAddress({\n          requestContext: execution.requestContext,\n          storage: options.storage,\n          sessions: options.sessions,\n        });\n        const currentAddress = currentResolution?.address ?? null;\n        const toolCallId = execution.agent?.toolCallId;\n        if (!currentAddress || !toolCallId) {\n          throw new Error('Factory transitions require an authenticated bound agent tool call.');\n        }\n        const binding = await options.storage.findActiveRunBinding(currentAddress);\n        // Authority is the work item this session is bound to, not the individual\n        // binding row. Handing the next role its turn in an existing session\n        // rotates the binding, and tools built for the previous role stay live\n        // across that rotation; keying on row identity would strand the run that\n        // the rotation exists to start. Re-pointing a session at a different item\n        // is the hijack this guards against.\n        if (!binding || binding.workItemId !== availableBinding.workItemId) {\n          throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n        }\n        const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n        if (!item) throw new Error('Bound Factory work item not found.');\n        const triageType =\n          'triageType' in input && isFactoryTriageType(input.triageType) ? input.triageType : undefined;\n\n        const result = await options.transitionService.transition({\n          orgId: binding.orgId,\n          factoryProjectId: binding.factoryProjectId,\n          workItemId: binding.workItemId,\n          board: boardForSource(item.externalSource?.type),\n          stage,\n          expectedRevision,\n          actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n          ingress: { type: 'agent', identity: `${binding.id}:${toolCallId}` },\n          cause: rationale,\n          ...(triageType ? { triageType } : {}),\n        });","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/tools.ts#L65-L101","documentation":"After re-resolving the session address, the tool looks up the active run binding for it and requires that the binding still exists and still points at the same work item observed when the tools were created. The binding is revoked when a run finishes/rotates roles, and re-pointing a session at a different item is treated as a hijack attempt, so a stale or mismatched tool call is rejected. Comment in source: authority is the work item this session is bound to, not the individual binding row.","triggerScenarios":"Calling the transition tool when `findActiveRunBinding(currentAddress)` returns nothing (binding expired, completed, or revoked), or returns a binding whose `workItemId` differs from `availableBinding.workItemId` captured at tool-creation time (e.g. the session was rotated to the next role or rebound to a different item).","commonSituations":"An agent retrying a queued/stale tool call after its run's turn ended and the binding rotated to the next role; a long-running agent whose session was rebound to a different work item mid-flight; duplicated/cached tools from a previous role kept live across a rotation; concurrent runs clobbering the binding.","solutions":["Do not reuse transition tools from a previous role/run: rebuild tools for the current role and let the new run call them fresh.","Re-start the run so a new binding is created before calling the tool; verify with findActiveRunBinding that it is active and matches the intended workItemId.","Check for concurrent transitions or double-kickoffs on the same session that rotate/revoke the binding mid-run; serialize runs per session.","Inspect the run binding table for the session address to confirm the current workItemId and role before debugging the agent."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const binding = await storage.findActiveRunBinding(address);\nconst stillValid = !!binding && binding.workItemId === availableBinding.workItemId;\nif (!stillValid) throw new Error('Binding rotated or revoked; rebuild tools and restart the run before transitioning.');","typeGuard":"function isCurrentBinding(\n  b: { workItemId: string } | null | undefined,\n  expectedWorkItemId: string,\n): b is { workItemId: string } {\n  return !!b && b.workItemId === expectedWorkItemId;\n}","tryCatchPattern":"try {\n  await transitionTool.execute(input, execution);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Factory agent binding is unavailable')) {\n    // binding rotated: end this run's attempts, wait for the new role turn\n  } else throw e;\n}","preventionTips":["Treat a completed/rotated run as terminal: do not reuse its tools or retry its tool calls.","Serialize runs per session so bindings are never rotated mid-run by a concurrent kickoff.","After any role handoff, rebuild the transition tools for the new role before executing.","Monitor findActiveRunBinding before kicking off to avoid double-bindings on one session."],"tags":["factory","authorization","session-binding","binding-revoked"],"backgroundTag":"session-binding-revoked","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}