mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] models: the per-session heal never moves a chat onto the free Haiku row; a dead login left only that row reachable and three chats were rewritten onto it (install 59a37510)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
fc7d063206
commit
d9f47051d5
@@ -12,6 +12,7 @@ import { fetchSettings, updateSettingsPatch, markFreeTrialArmSettled } from '@/s
|
||||
import { fetchSubscriptionStatus } from '@/shared/state/subscriptionsSlice';
|
||||
import { fetchModels } from '@/shared/state/modelsSlice';
|
||||
import { updateSessionModel, persistSessionModel } from '@/shared/state/agentsSlice';
|
||||
import { healTarget } from './modelHealTarget';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import {
|
||||
setAppVersion,
|
||||
@@ -302,7 +303,15 @@ const DefaultModelGuard: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
return;
|
||||
}
|
||||
const known = new Set(knownValues);
|
||||
const target = valid.has(settings.default_model) ? settings.default_model : fallback.value;
|
||||
const p_target = healTarget(flat, settings.default_model, fallback);
|
||||
if (!p_target) {
|
||||
if (!warnedSessionsRef.current.has('free-row-only')) {
|
||||
warnedSessionsRef.current.add('free-row-only');
|
||||
console.warn('[models] only the free row is reachable; chats on retired models stay where they are this tick');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const target = p_target.value;
|
||||
let switched = false;
|
||||
for (const sess of Object.values(store.getState().agents.sessions)) {
|
||||
if (sess.model && !known.has(sess.model)) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { healTarget } from './modelHealTarget';
|
||||
|
||||
const sonnet = { value: 'sonnet-5-cc', label: 'Claude Sonnet 5', billing_kind: 'subscription' };
|
||||
const haiku = { value: 'haiku', label: 'Claude Haiku 4.5', billing_kind: 'free' };
|
||||
|
||||
test('the stored default wins when it is reachable', () => {
|
||||
assert.deepEqual(healTarget([sonnet, haiku], 'sonnet-5-cc', { value: 'x', label: 'X' }), { value: 'sonnet-5-cc', label: 'Claude Sonnet 5' });
|
||||
});
|
||||
|
||||
test('otherwise the priority fallback', () => {
|
||||
assert.deepEqual(healTarget([sonnet], 'gpt-5.4', { value: 'sonnet-5-cc', label: 'Claude Sonnet 5' }), { value: 'sonnet-5-cc', label: 'Claude Sonnet 5' });
|
||||
});
|
||||
|
||||
test('when only the free row is reachable nothing is moved (the 1.7.9 Haiku rewrite)', () => {
|
||||
assert.equal(healTarget([haiku], 'opus-5', { value: 'haiku', label: 'Claude Haiku 4.5' }), null);
|
||||
assert.equal(healTarget([], 'opus-5', null), null);
|
||||
});
|
||||
|
||||
test('the per-session heal in Main.tsx decides its target through healTarget', async () => {
|
||||
const fs = await import('node:fs');
|
||||
const url = await import('node:url');
|
||||
const here = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
const candidates = [here + 'Main.tsx', here.replace(/([\\/])\.test-build([\\/])/, '$1src$2') + 'Main.tsx'];
|
||||
const path = candidates.find((p) => fs.existsSync(p));
|
||||
assert.ok(path, `could not locate Main.tsx from ${here}`);
|
||||
const src = fs.readFileSync(path as string, 'utf8');
|
||||
assert.ok(src.includes('healTarget(flat, settings.default_model, fallback)'), 'the heal must ask healTarget, not pick a fallback on its own');
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface HealModel { value: string; label: string; billing_kind?: string }
|
||||
|
||||
/** Where a chat pinned to a retired model is moved, or null when nothing of the user's own is reachable
|
||||
* this tick. On 1.7.9 a dead Claude login left only the funded Haiku row in the list and every chat was
|
||||
* rewritten onto it (install 59a37510, 2026-09-03: three chats, then "No credentials for provider: claude"
|
||||
* on a model nobody picked). A free row is a face for the picker, never a home for someone's chat. */
|
||||
export function healTarget(
|
||||
reachable: HealModel[],
|
||||
defaultModel: string,
|
||||
fallback: { value: string; label: string } | null,
|
||||
): { value: string; label: string } | null {
|
||||
if (!reachable.some((m) => m.billing_kind !== 'free')) return null;
|
||||
const current = reachable.find((m) => m.value === defaultModel);
|
||||
if (current) return { value: current.value, label: current.label };
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user