Bug type
Behavior bug / partial state cleanup
Beta release blocker
No
Summary
sessions.patch with model: null returns success and clears modelOverride, providerOverride, and modelOverrideSource from the session entry, but leaves liveModelSwitchPending: true on disk. The orphan flag causes downstream resolvers (e.g. /model rendering) to reconstruct a ghost selection from the session's .jsonl trajectory, which produces nonsensical model labels in chat when the originally-pinned model/provider no longer exists in config.
This is distinct from #69951 (override silently ignored), #77322 (session.model cache survives /new), and #73090 (modelProvider metadata not normalized for CLI runtimes).
Environment
- OpenClaw:
2026.5.12 (f066dd2)
- Install: npm global (
/usr/lib/node_modules/openclaw)
- OS: Zorin OS 18.1 (Linux 6.17, x86_64)
- Gateway: local, systemd --user
- Channel where ghost label was observed: Telegram DM
Steps to reproduce
Pure gateway-call repro, no UI required.
SK='{"key":"agent:main:main"}'
# T0: confirm clean state
python3 -c "
import json
s = json.load(open('$HOME/.openclaw/agents/main/sessions/sessions.json'))['agent:main:main']
for k in ('modelOverride','providerOverride','modelOverrideSource','liveModelSwitchPending'):
print(f'{k}: {s.get(k, \"<unset>\")}')
"
# T1: pin a model
openclaw gateway call sessions.patch --params \
'{"key":"agent:main:main","model":"openai/gpt-5.5"}'
# T2: confirm all 4 fields are set
python3 -c "... (same as T0)"
# Output:
# modelOverride: gpt-5.5
# providerOverride: openai
# modelOverrideSource: user
# liveModelSwitchPending: True
# T3: clear pin
openclaw gateway call sessions.patch --params \
'{"key":"agent:main:main","model":null}'
# T4: re-check fields
python3 -c "... (same as T0)"
Expected behavior
T4 should show all four fields as <unset>. sessions.patch model:null is the documented way to clear a pinned model and should leave the entry in the same shape as a fresh session.
Actual behavior
T4 output:
modelOverride: <unset> ✓ cleared
providerOverride: <unset> ✓ cleared
modelOverrideSource: <unset> ✓ cleared
liveModelSwitchPending: True ✗ ORPHANED
The orphan liveModelSwitchPending: true survives indefinitely until either (a) another sessions.patch with a concrete model triggers a successful live switch (which clears it via clearLiveModelSwitchPending), or (b) manual JSON edit.
Observed downstream impact
In my session this morning, the orphan flag manifested when sending /model in Telegram. The handler showed:
qwen-7b was an alias I had defined hours earlier on a now-removed llm-vps provider. The provider entry and the alias had both been deleted from openclaw.json and the gateway restarted. The only remaining trace was the orphan liveModelSwitchPending: true on agent:main:main. The handler's resolveLiveSessionModelSelection path apparently mined the session's .jsonl trajectory for the most recent /new-style switch, found "qwen-7b" (now an unknown identifier), and fell back to the default provider (openai) to render the bogus openai/qwen-7b label.
Removing liveModelSwitchPending: true manually from the entry, then re-rendering /model, immediately produced the correct Current: openai/gpt-5.
Suggested fix direction
In the same code path that handles sessions.patch with model: null (clearing the pin), also call clearLiveModelSwitchPending for that session key. The function already exists at live-model-switch-CgybHLBX.js (bundled name) and is what model_spawn and live-switch retries use to consume the flag — it should be the canonical drain on explicit-clear as well.
A defensive measure in formatCurrentModelLine / resolveLiveSessionModelSelection (don't synthesize a selection from trajectory when the override fields are unset) would also prevent ghost labels even if the orphan flag persists for unrelated reasons.
Bug type
Behavior bug / partial state cleanup
Beta release blocker
No
Summary
sessions.patchwithmodel: nullreturns success and clearsmodelOverride,providerOverride, andmodelOverrideSourcefrom the session entry, but leavesliveModelSwitchPending: trueon disk. The orphan flag causes downstream resolvers (e.g./modelrendering) to reconstruct a ghost selection from the session's.jsonltrajectory, which produces nonsensical model labels in chat when the originally-pinned model/provider no longer exists in config.This is distinct from #69951 (override silently ignored), #77322 (
session.modelcache survives/new), and #73090 (modelProvider metadata not normalized for CLI runtimes).Environment
2026.5.12 (f066dd2)/usr/lib/node_modules/openclaw)Steps to reproduce
Pure gateway-call repro, no UI required.
Expected behavior
T4 should show all four fields as
<unset>.sessions.patch model:nullis the documented way to clear a pinned model and should leave the entry in the same shape as a fresh session.Actual behavior
T4 output:
The orphan
liveModelSwitchPending: truesurvives indefinitely until either (a) anothersessions.patchwith a concrete model triggers a successful live switch (which clears it viaclearLiveModelSwitchPending), or (b) manual JSON edit.Observed downstream impact
In my session this morning, the orphan flag manifested when sending
/modelin Telegram. The handler showed:qwen-7bwas an alias I had defined hours earlier on a now-removedllm-vpsprovider. The provider entry and the alias had both been deleted fromopenclaw.jsonand the gateway restarted. The only remaining trace was the orphanliveModelSwitchPending: trueonagent:main:main. The handler'sresolveLiveSessionModelSelectionpath apparently mined the session's.jsonltrajectory for the most recent/new-style switch, found "qwen-7b" (now an unknown identifier), and fell back to the default provider (openai) to render the bogusopenai/qwen-7blabel.Removing
liveModelSwitchPending: truemanually from the entry, then re-rendering/model, immediately produced the correctCurrent: openai/gpt-5.Suggested fix direction
In the same code path that handles
sessions.patchwithmodel: null(clearing the pin), also callclearLiveModelSwitchPendingfor that session key. The function already exists atlive-model-switch-CgybHLBX.js(bundled name) and is whatmodel_spawnand live-switch retries use to consume the flag — it should be the canonical drain on explicit-clear as well.A defensive measure in
formatCurrentModelLine/resolveLiveSessionModelSelection(don't synthesize a selection from trajectory when the override fields are unset) would also prevent ghost labels even if the orphan flag persists for unrelated reasons.