#23053·mastra

Factory: a degraded agent session permanently fails every rule-driven run with "No model available: this run started without a controller session context"

Author: Andy-HaighCreated Sep 4, 2026Updated Sep 17, 2026
Labelsbugimpact:higheffort:mediumstatus: auto-triaged@mastra/core

Summary

On a self-hosted Factory, a long-lived agent session can reach a state where every subsequent rule-driven run dispatched onto it fails at model resolution with:

No model available: this run started without a controller session context, so no model selection could be resolved.

Once a session is in this state nothing in the Factory recovers from it. The work item is stuck at its current revision indefinitely — in our case a single PR was re-dispatched every 15 minutes for a full day, ~60 failed dispatches, before we restarted the process. Restarting the server is the only recovery we found.

Where it throws

@mastra/code-sdk/dist/agents/model.js, getDynamicModel:

javascript
function getDynamicModel({ requestContext }, settingsPath) {
	const agentControllerContext = requestContext.get("controller");
	const modelId = agentControllerContext?.session?.modelId;
	if (!modelId) {
		if (!agentControllerContext) throw new Error("No model available: this run started without a controller session context, so no model selection could be resolved.");
		throw new Error("No model selected. Use /models to select a model first.");
	}
	...
}

The message is reached only when requestContext.get("controller") is undefined entirely — i.e. the request context never went through AgentController#buildRequestContext. It is not the "no model selected" branch.

This is not a credentials problem

We spent a while on that assumption first, so it seems worth stating: the throw is above any credential lookup. Re-authenticating the Anthropic provider (a fresh credential was stored and confirmed in the DB) changed nothing — the next dispatch failed identically.

It is never the first failure

This is the most useful signal we have. Across our factory_deferred_decisions table, grouped by attempt count:

error class attempts count
clean (succeeded) 0 24
clean (succeeded) 1 118
clean (succeeded) 2 7
No model available 3 2
No model available 5 115
other errors 1–6 365

No model available never appears at attempt 1 or 2 — only at 3 and 5, i.e. only on the dispatcher's retries. Something else kills the first attempt (in our data usually Factory skill run was aborted before it finished or Factory skill invocation was queued onto an ending run and never reached the agent), and what the retry then finds is a session that can no longer resolve a model.

So the bug looks like a session-lifecycle one: after a run is aborted or ends abnormally, the session object survives in the controller's registry but is no longer capable of building a controller context for a new run.

The dispatcher cannot route around it

RuleDispatcher#requireOrPrepareBinding only prepares a fresh binding when the existing binding's session is absent:

javascript
const binding = await this.#findBinding(record, role);
if (binding) {
	if (await this.#controller.getSessionByResource(binding.resourceId)) return binding;
}

Here getSessionByResource returns the degraded session, so the stale binding is kept. We tried revoking the active factory_run_bindings row by hand to force prepareBinding; it created a new binding row but with the same thread_id / session_id, and the next dispatch failed identically. There is no path back short of a process restart.

Both resolvePromptInvocation and resolveSkillInvocation are affected — we see it on invokeSkill decisions carrying a bare prompt and on ones carrying a skillName, across triage, plan and review roles.

Not a 0.12.0 regression

We noticed it after upgrading @mastra/factory 0.9.0-alpha.2 → 0.12.0 and assumed the upgrade caused it. It did not: our earliest occurrence is 2026-08-26, on @mastra/factory 0.9.0-alpha.2, a week before the upgrade.

Versions

  • @mastra/factory 0.12.0 (also seen on 0.9.0-alpha.2)
  • @mastra/code-sdk 1.6.0
  • @mastra/core 1.64.0
  • Self-hosted from the softwarefactory-template, Postgres + Redis, MASTRACODE_DISPATCH_MAX_IN_FLIGHT=3, Node 24

What would help

Any one of these would be enough to make this self-healing rather than terminal:

  1. Treat "session cannot build a controller context" as session absent in getSessionByResource, so requireOrPrepareBinding replaces it.
  2. Evict a session from the controller registry when its run ends abnormally, rather than leaving it registered in a state it cannot serve from.
  3. Failing either, make the error non-retryable and distinguishable, so a consumer can detect it and recycle the session itself — right now it is indistinguishable from a transient at the dispatch layer, and the retry budget is spent on a condition retries cannot fix.

Possibly related, though the symptom and error string differ: #20888 (dispatcher never rehydrates sessions after a restart).