Skip to content

Commit 4f7b72c

Browse files
fix(runtime-host,storage): migrate untouched legacy opencode-free seeds atomically
Existing installs never re-ran bootstrap seeding, so upgrades kept retired ids and never received newly derived free models. Built-in seed evolution is one connection-catalog-authority CAS mutation (migrateSystemSeed): a row whose enabledModelIds still exactly match a historical system seed follows the current seed, its static inventory is re-derived from the current build, and a default target the migration removes is retargeted inside the same document write — so no restart can observe enabled ids without their inventory, or a nulled default awaiting a second write. Any other inventory (including a reordering) is a user selection and is never touched; an already-null default stays null. Idempotent by construction. The exact-match enumeration is deliberately lossy and every seed change must append the prior value; the versioned seed policy discussed in #3354 is the durable replacement. Tests start from an actual pre-#3409 persisted document and verify both completion and the restart no-op on the far side of the single commit boundary. Refs #3409 Generated-by: Claude Code
1 parent b4a1c9f commit 4f7b72c

6 files changed

Lines changed: 187 additions & 47 deletions

File tree

packages/core/src/runtime-policy.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,21 @@ export interface RemoveCatalogConnectionInput {
276276
readonly expected: ConnectionVersionBasis;
277277
}
278278

279+
/**
280+
* Built-in seed evolution as one atomic catalog mutation: a row still exactly
281+
* matching a historical system seed follows the current seed — enabled ids AND
282+
* the static inventory — and a system default the migration removes is
283+
* retargeted in the same document write. Any other inventory is a user
284+
* selection and is never touched; an already-null default stays null.
285+
*/
286+
export interface MigrateSystemSeedInput {
287+
readonly slug: string;
288+
readonly providerType: ProviderType;
289+
readonly legacyEnabledModelIds: readonly (readonly string[])[];
290+
readonly enabledModelIds: readonly string[];
291+
readonly defaultModelId: string;
292+
}
293+
279294
export interface SetDefaultConnectionTargetInput {
280295
readonly expectedCatalogRevision: Revision;
281296
readonly target: ConnectionTarget | null;

packages/runtime-host/src/__tests__/bootstrap-runtime-policy.test.ts

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -163,71 +163,96 @@ test('an invalid optional environment credential does not keep bootstrap active'
163163
});
164164
});
165165

166-
test('an untouched legacy opencode-free seed follows the current seed', async () => {
166+
test('a historical persisted seed migrates atomically, inventory and default included', async () => {
167167
await withFixture(async ({ root, stores }) => {
168-
const created = await stores.connectionCatalog.create({
169-
expectedCatalogRevision: 0,
170-
connection: {
171-
slug: 'opencode-free',
172-
name: 'OpenCode Free',
173-
providerType: 'opencode-free',
174-
enabled: true,
175-
enabledModelIds: ['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'],
176-
},
177-
});
178-
assert.equal(created.kind, 'committed');
168+
// An actual pre-#3409 persisted document: the three-model seed, the
169+
// pinned four-model fallback inventory of that build, and a default
170+
// target on a model the migration removes.
171+
const connectionId = '00000000-0000-4000-8000-000000000001';
172+
await writeFile(
173+
join(root, 'connection-catalog.json'),
174+
`${JSON.stringify({
175+
schemaVersion: 1,
176+
revision: 7,
177+
defaultTarget: { connectionId, modelId: 'deepseek-v4-flash-free' },
178+
connections: [
179+
{
180+
connectionId,
181+
revision: 3,
182+
slug: 'opencode-free',
183+
name: 'OpenCode Free',
184+
providerType: 'opencode-free',
185+
enabled: true,
186+
enabledModelIds: ['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'],
187+
models: [
188+
{ id: 'nemotron-3-ultra-free' },
189+
{ id: 'mimo-v2.5-free' },
190+
{ id: 'big-pickle' },
191+
{ id: 'deepseek-v4-flash-free' },
192+
],
193+
modelSource: 'fallback',
194+
modelsFetchedAt: 0,
195+
},
196+
],
197+
})}\n`,
198+
);
179199

180200
await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} });
181201

202+
// One document write carried all three: enabled ids, the re-derived
203+
// static inventory, and the retargeted default.
182204
const catalog = await stores.connectionCatalog.getSnapshot();
183-
const free = catalog.connections.find(({ slug }) => slug === 'opencode-free');
184-
assert.deepEqual(free?.enabledModelIds, [...OPENCODE_FREE_DEFAULT_ENABLED_MODELS]);
205+
const migrated = catalog.connections.find(({ slug }) => slug === 'opencode-free');
206+
assert.deepEqual(migrated?.enabledModelIds, [...OPENCODE_FREE_DEFAULT_ENABLED_MODELS]);
207+
assert.deepEqual(
208+
migrated?.models.map(({ id }) => id),
209+
[...OPENCODE_FREE_DEFAULT_ENABLED_MODELS],
210+
);
211+
assert.deepEqual(catalog.defaultTarget, {
212+
connectionId,
213+
modelId: OPENCODE_FREE_DEFAULT_MODEL,
214+
});
185215

186-
// Idempotent: a second bootstrap leaves the migrated row unchanged.
216+
// Restart on the far side of the commit boundary: a second bootstrap is a
217+
// byte-identical no-op. (On the near side the single write never happened,
218+
// so the original document simply migrates on the next start.)
187219
await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} });
188220
assert.deepEqual(await stores.connectionCatalog.getSnapshot(), catalog);
189221
});
190222
});
191223

192-
test('a legacy seed migrates and repairs a default target the migration nulled', async () => {
224+
test('a historical seed with a user-cleared default migrates without inventing one', async () => {
193225
await withFixture(async ({ root, stores }) => {
194-
const created = await stores.connectionCatalog.create({
195-
expectedCatalogRevision: 0,
196-
connection: {
197-
slug: 'opencode-free',
198-
name: 'OpenCode Free',
199-
providerType: 'opencode-free',
200-
enabled: true,
201-
enabledModelIds: ['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'],
202-
},
203-
});
204-
assert.equal(created.kind, 'committed');
205-
if (created.kind !== 'committed') return;
206-
const free = created.snapshot.connections[0]!;
207-
// Default target on a model the migration removes: the retained-target
208-
// rules null it, and bootstrap must re-seed it.
209-
const targeted = await stores.connectionCatalog.setDefaultTarget({
210-
expectedCatalogRevision: created.snapshot.revision,
211-
target: { connectionId: free.connectionId, modelId: 'deepseek-v4-flash-free' },
212-
});
213-
assert.equal(targeted.kind, 'committed');
226+
const connectionId = '00000000-0000-4000-8000-000000000002';
227+
await writeFile(
228+
join(root, 'connection-catalog.json'),
229+
`${JSON.stringify({
230+
schemaVersion: 1,
231+
revision: 4,
232+
defaultTarget: null,
233+
connections: [
234+
{
235+
connectionId,
236+
revision: 2,
237+
slug: 'opencode-free',
238+
name: 'OpenCode Free',
239+
providerType: 'opencode-free',
240+
enabled: true,
241+
enabledModelIds: ['nemotron-3-ultra-free'],
242+
models: [{ id: 'nemotron-3-ultra-free' }],
243+
modelSource: 'fallback',
244+
modelsFetchedAt: 0,
245+
},
246+
],
247+
})}\n`,
248+
);
214249

215250
await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} });
216251

217252
const catalog = await stores.connectionCatalog.getSnapshot();
218253
const migrated = catalog.connections.find(({ slug }) => slug === 'opencode-free');
219254
assert.deepEqual(migrated?.enabledModelIds, [...OPENCODE_FREE_DEFAULT_ENABLED_MODELS]);
220-
// The migration write also refreshes the stored inventory to the current
221-
// build's candidates exactly, so every model the picker offers is one
222-
// execution admission accepts and no stale id survives.
223-
assert.deepEqual(
224-
migrated?.models.map(({ id }) => id),
225-
[...OPENCODE_FREE_DEFAULT_ENABLED_MODELS],
226-
);
227-
assert.deepEqual(catalog.defaultTarget, {
228-
connectionId: free.connectionId,
229-
modelId: OPENCODE_FREE_DEFAULT_MODEL,
230-
});
255+
assert.equal(catalog.defaultTarget, null);
231256
});
232257
});
233258

packages/runtime-host/src/server/bootstrap-runtime-policy.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ export async function ensureBootstrapRuntimePolicy(input: {
4141
const journalPath = join(input.workspaceRoot, JOURNAL_FILE);
4242
const resuming = await readJournal(journalPath);
4343
const initialCatalog = await input.stores.connectionCatalog.getSnapshot();
44+
if (initialCatalog.connections.length > 0) {
45+
// One atomic catalog mutation: enabled ids, static inventory, and a
46+
// retarget of a system default the migration removes all land in the same
47+
// document write, so no restart can observe a half-migrated row.
48+
try {
49+
await input.stores.connectionCatalog.migrateSystemSeed({
50+
slug: 'opencode-free',
51+
providerType: 'opencode-free',
52+
legacyEnabledModelIds: LEGACY_OPENCODE_FREE_SEEDS,
53+
enabledModelIds: OPENCODE_FREE_DEFAULT_ENABLED_MODELS,
54+
defaultModelId: OPENCODE_FREE_DEFAULT_MODEL,
55+
});
56+
} catch (error) {
57+
input.onDeferredError?.(error);
58+
}
59+
}
4460
if (!resuming) {
4561
if (initialCatalog.connections.length > 0) return;
4662
await writeJournal(journalPath);
@@ -146,6 +162,21 @@ async function ensureConnection(
146162
throw new Error(`Bootstrap Connection could not be created: ${seed.slug}`);
147163
}
148164

165+
/**
166+
* Every opencode-free inventory a past release seeded, verbatim. A row still
167+
* equal to one of these is provably system-owned and may follow the current
168+
* seed; any other value is a user selection and is never touched. Every
169+
* release that changes the derived seed must append the previous value here,
170+
* or rows it planted read as user selections forever. (This exact-match
171+
* enumeration is deliberately lossy; the versioned seed policy discussed in
172+
* #3354 is the durable replacement.)
173+
*/
174+
const LEGACY_OPENCODE_FREE_SEEDS: readonly (readonly string[])[] = [
175+
['big-pickle'],
176+
['nemotron-3-ultra-free'],
177+
['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'],
178+
];
179+
149180
async function removeFailedBootstrapConnection(
150181
stores: RuntimePolicyStoresWriter,
151182
connection: ConnectionCatalogEntry,

packages/storage/src/runtime-policy-stores.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
RemoveCatalogConnectionInput,
1212
RuntimePolicySnapshot,
1313
SetCredentialInput,
14+
MigrateSystemSeedInput,
1415
SetDefaultConnectionTargetInput,
1516
UpdateCatalogConnectionInput,
1617
} from '@maka/core/runtime-policy';
@@ -87,6 +88,7 @@ export interface ConnectionCatalogWriter extends ConnectionCatalogReader {
8788
setDefaultTarget(
8889
input: SetDefaultConnectionTargetInput,
8990
): Promise<ConnectionCatalogMutationResult>;
91+
migrateSystemSeed(input: MigrateSystemSeedInput): Promise<ConnectionCatalogMutationResult>;
9092
}
9193

9294
export interface CredentialVaultReader {
@@ -200,6 +202,7 @@ function createWriterFacade(coordinator: RuntimePolicyCoordinator): RuntimePolic
200202
update: (input) => coordinator.updateConnection(input),
201203
remove: (input) => coordinator.removeConnection(input),
202204
setDefaultTarget: (input) => coordinator.setDefaultTarget(input),
205+
migrateSystemSeed: (input) => coordinator.migrateSystemSeed(input),
203206
},
204207
credentialVault: {
205208
getSnapshot: () => coordinator.getVaultSnapshot(),

packages/storage/src/runtime-policy/connection-catalog-document.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type CreateCatalogConnectionInput,
2626
type RemoveCatalogConnectionInput,
2727
type SetDefaultConnectionTargetInput,
28+
type MigrateSystemSeedInput,
2829
type UpdateCatalogConnectionInput,
2930
} from '@maka/core/runtime-policy';
3031
import {
@@ -310,6 +311,66 @@ export class ConnectionCatalogDocumentOwner {
310311
return committed(next);
311312
}
312313

314+
/**
315+
* Built-in seed evolution as ONE atomic catalog mutation. A row whose
316+
* `enabledModelIds` still exactly match a historical system seed is provably
317+
* system-owned: it follows the current seed, its static inventory is
318+
* re-derived from the current build, and a default target the migration
319+
* removes is retargeted inside the same document write — so no restart can
320+
* observe enabled ids without their inventory, or a nulled default awaiting
321+
* a second write. Any other inventory (including a reordering) is a user
322+
* selection and is never touched; an already-null default stays null.
323+
*/
324+
async migrateSystemSeed(
325+
root: string,
326+
input: MigrateSystemSeedInput,
327+
): Promise<ConnectionCatalogMutationResult> {
328+
if (!input.enabledModelIds.includes(input.defaultModelId)) {
329+
throw codecError('invalid_connection_input', 'Seed default must be in the seed selection');
330+
}
331+
const current = await this.read(root);
332+
const index = current.connections.findIndex(
333+
(item) => item.slug === input.slug && item.providerType === input.providerType,
334+
);
335+
const previous = current.connections[index];
336+
const sameIds = (left: readonly string[], right: readonly string[]) =>
337+
left.length === right.length && left.every((id, position) => id === right[position]);
338+
if (
339+
!previous ||
340+
sameIds(previous.enabledModelIds, input.enabledModelIds) ||
341+
!input.legacyEnabledModelIds.some((seed) => sameIds(previous.enabledModelIds, seed))
342+
) {
343+
return committed(current);
344+
}
345+
const fallbackModels = fallbackInventory(previous.providerType);
346+
const {
347+
lastTest: _lastTest,
348+
modelSource: _modelSource,
349+
modelsFetchedAt: _modelsFetchedAt,
350+
...retained
351+
} = previous;
352+
const connections = [...current.connections];
353+
connections[index] = {
354+
...retained,
355+
revision: nextRevision(previous.revision),
356+
enabledModelIds: [...input.enabledModelIds],
357+
models: fallbackModels,
358+
...(fallbackModels.length > 0
359+
? { modelSource: 'fallback' as const, modelsFetchedAt: 0 }
360+
: {}),
361+
};
362+
const target = current.defaultTarget;
363+
const defaultTarget =
364+
target !== null &&
365+
target.connectionId === previous.connectionId &&
366+
!input.enabledModelIds.includes(target.modelId)
367+
? { connectionId: previous.connectionId, modelId: input.defaultModelId }
368+
: target;
369+
const next = this.nextDocument(current, connections, defaultTarget);
370+
await this.write(root, next);
371+
return committed(next);
372+
}
373+
313374
async setDefaultTarget(
314375
root: string,
315376
rawInput: SetDefaultConnectionTargetInput,

packages/storage/src/runtime-policy/coordinator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type RequestHeaderUpdate,
3030
type SavedRequestHeaders,
3131
type SetCredentialInput,
32+
type MigrateSystemSeedInput,
3233
type SetDefaultConnectionTargetInput,
3334
type UpdateCatalogConnectionInput,
3435
} from '@maka/core/runtime-policy';
@@ -296,6 +297,10 @@ export class RuntimePolicyCoordinator {
296297
return this.inLane((root) => this.catalog.setDefaultTarget(root, input));
297298
}
298299

300+
migrateSystemSeed(input: MigrateSystemSeedInput) {
301+
return this.inLane((root) => this.catalog.migrateSystemSeed(root, input));
302+
}
303+
299304
setCredential(rawInput: SetCredentialInput) {
300305
return this.setCredentialWithAuthority(rawInput, 'client');
301306
}

0 commit comments

Comments
 (0)