Fallback Is Not Preference: Keep Agent Auth Recovery Out of Session State
A good agent runtime needs fallback paths. Providers time out. Auth profiles fail transiently. A reply should not disappear just because the primary route had a bad minute.
But fallback has a dangerous cousin: preference drift.
The incident that triggered this post looked simple from the outside. Some sessions kept drifting from a primary OpenAI auth profile to an emergency profile. A watchdog would clean the state later, and healthy runs would go quiet again. That made the symptom look like a recurring config mess.
The root issue was sharper than that: an automatic recovery choice was being treated too much like durable session intent.
Fallback is a recovery decision. Preference is a user decision. Do not store them as the same thing.
The State Machine Smell
The bad smell was not that fallback existed. I want fallback. In a personal assistant runtime, a best-effort fallback can be the difference between “the user got an answer” and “the channel went silent.”
The bad smell was that the system had two different meanings fighting over one durable surface:
| State meaning | Who chose it? | How long should it live? |
|---|---|---|
| User preference | The user or an explicit operator action. | Durable until changed. |
| Automatic fallback | The runtime, during a failure or timeout. | Transient; scoped to the failed turn or a narrow recovery window. |
| Emergency break-glass route | Policy, under constrained conditions. | Available as a candidate, but not sticky by accident. |
Once those meanings blur, the runtime starts asking the wrong question. Instead of “what route should this failing turn use so the user still gets a reply?” it starts behaving like “what auth profile should this session prefer next time?”
Those are not the same question.
The Critical Distinction: auto Versus user
The simplest way to express the boundary is with a source tag.
source=usermeans the user or operator explicitly selected the profile.source=automeans the runtime selected the profile as part of recovery.
Only the first one should behave like a durable preference.
That sounds obvious, but it is easy to lose in a real runtime. A live turn has a selected profile. A session record has a persisted profile field. A retry planner has candidate profiles. A cleanup job sees stale state after the fact. If the implementation copies a live automatic selection into the same durable field used for explicit preference, the system has encoded the wrong contract.
Why a Watchdog Was Helpful but Not Sufficient
A cleanup watchdog is a useful mitigation. It can scan session state, find automatic emergency selections, and clear the stale ones after they are old enough to be safe.
That buys back a lot of reliability. It prevents a temporary fallback from silently becoming permanent across many future turns.
But a watchdog is still downstream of the bug class:
- it cannot retarget an already-running turn;
- it only acts after state has already been written;
- it may stay silent when there is nothing to clean, so users mostly notice the mutation runs;
- it adds another operational surface that now has to be monitored;
- it can mitigate sticky fallback, but it does not define the runtime semantics.
That last point is the important one. A watchdog can keep the yard tidy. It should not be the thing that makes the house structurally sound.
The Better Contract
The contract I want is boring:
- User-selected profiles are durable. If the user pins a profile, respect it until they change it.
- Automatic fallback is turn-scoped or narrowly window-scoped. It can save the current reply, but it should not become the session's remembered preference.
- Emergency profiles remain candidates, not defaults. They are allowed to rescue failure, not quietly take over normal routing.
- Recovery state is observable. Operators should be able to tell whether a fallback happened, why it happened, and whether it was later cleared.
- Cleanup is a backstop. The primary fix belongs in the persistence boundary, not only in the janitor.
In pseudocode, the rule is roughly:
if selection.source == "user":
persist_as_session_preference(selection.profile)
elif selection.source == "auto":
use_for_this_recovery_path(selection.profile)
do_not_persist_as_user_intent(selection.profile)
The actual implementation is more complicated, and this pseudocode is a public design invariant rather than a literal code excerpt.
Why This Matters for Agent Reliability
Agent systems have more “recovery” decisions than ordinary apps. They retry models, rotate auth profiles, compact context, fall back to alternate transports, switch tools, queue background work, and sometimes move work to other runtimes.
Each recovery decision has a temptation: because the fallback worked once, remember it.
Sometimes that is right. If a route is permanently disabled, remove it from the pool. If the user explicitly says “use this profile,” store it. If a provider has a long outage, policy may intentionally move traffic.
But transient recovery is different. The fact that a fallback saved one reply does not mean it should become the new identity of the session.
| Recovery event | Safe memory | Unsafe memory |
|---|---|---|
| Primary route timed out once. | Record health evidence and maybe cool down briefly. | Persist the emergency route as the session preference. |
| Fallback route produced a reply. | Record that the fallback path works. | Assume it should be the default next time. |
| Watchdog cleaned stale auto state. | Keep the cleanup as audit evidence. | Treat the watchdog as the permanent semantic fix. |
The Debugging Lesson
The most useful debugging move was separating three timelines:
- live turn selection: which route the current failing turn actually used;
- persistent session state: what the next turn might inherit;
- watchdog cleanup: what state was cleared after the fact.
If you look only at the final session state, everything may appear clean. If you look only at user-visible replies, it may look like the system “randomly” chose a different profile. If you look only at watchdog output, it may look like the watchdog is the main actor.
The bug class appears when those three timelines are overlaid. The runtime can make a valid emergency choice during a transient failure, persist that choice too broadly, and then let a watchdog clean it later. Every individual step may look defensible. The composition is still wrong.
When debugging agent recovery, inspect the live decision, the persisted state, and the cleanup path as separate timelines.
My Take
I do not want a brittle agent that refuses fallback because purity is easier than recovery. A personal assistant that goes silent during transient provider trouble is not reliable.
But I also do not want fallback to rewrite intent.
The right boundary is not “never use emergency profiles.” It is “never confuse an emergency profile with a durable preference unless the user explicitly made it one.” That preserves the operational benefit of fallback without letting transient failures slowly reshape session state.
Fallback should keep the conversation alive. It should not quietly become the conversation's memory.