diff --git a/electron/coding-runtime/pi/provider-config.ts b/electron/coding-runtime/pi/provider-config.ts index a08b9ba..6b11ad8 100644 --- a/electron/coding-runtime/pi/provider-config.ts +++ b/electron/coding-runtime/pi/provider-config.ts @@ -22,6 +22,7 @@ import { getImportedModelProfile } from '../../../shared/imported-model-profile' const PI_ENV_PREFIX = 'MAKELORE_PI'; const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway'; const WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE = 'works_square_ai_gateway_proxy'; +const providerCatalogWriteTails = new Map>(); export const PI_PROVIDER_APIS = [ 'openai-completions', @@ -469,7 +470,18 @@ export async function writePiProviderCatalog( modelRef?: ProductModelRef, ): Promise { if (modelRef) selectPiProviderModel(catalog, modelRef); - await atomicWriteJson(filePath, catalog.modelsFile); + const previous = providerCatalogWriteTails.get(filePath) ?? Promise.resolve(); + const write = previous + .catch(() => undefined) + .then(async () => await atomicWriteJson(filePath, catalog.modelsFile)); + providerCatalogWriteTails.set(filePath, write); + try { + await write; + } finally { + if (providerCatalogWriteTails.get(filePath) === write) { + providerCatalogWriteTails.delete(filePath); + } + } } function replaceCredentialEnvReferences(value: string, credential: string | null): string { diff --git a/electron/coding-runtime/pi/release-proof.ts b/electron/coding-runtime/pi/release-proof.ts index 757c4f8..d0ef36f 100644 --- a/electron/coding-runtime/pi/release-proof.ts +++ b/electron/coding-runtime/pi/release-proof.ts @@ -144,7 +144,7 @@ const EXPECTED_TURN_MILESTONES: readonly ProofMilestone[] = [ let pressureRun: PressureRun | null = null; async function waitFor(predicate: () => boolean, message: string): Promise { - const deadline = Date.now() + 15_000; + const deadline = Date.now() + 30_000; while (Date.now() < deadline) { if (predicate()) return; await new Promise((resolveWait) => setTimeout(resolveWait, 20)); @@ -350,7 +350,7 @@ async function createProofProjects( for (let index = 0; index < count; index += 1) { const number = index + 1; const projectPath = path.join(root, `project-${number}`); - const project = await createLocalCodingProject({ projectPath, now }, projectStore); + const { project } = await createLocalCodingProject({ projectPath, now }, projectStore); await createCodingProjectAgent(projectPath, { id: PROOF_AGENT_ID, avatarId: 'avatar-01', @@ -661,7 +661,7 @@ async function startPressureRun(): Promise { `release-proof-write-project-${index + 1}`, `release-proof-holder-${index + 1}`, )); - dispatches.push(composition.scheduler.dispatch({ + dispatches.push(composition.scheduler.dispatch({ conversationId: project.input.conversationId, workerGeneration: generation, runId, @@ -676,13 +676,31 @@ async function startPressureRun(): Promise { }, })); } - await waitFor( - () => composition.processBudget.activeCount === 8 - && composition.scheduler.getDiagnostics().activeChildPermits === 4 - && provider.activeCounts().child === 4 - && processIds(composition.tracked, 'child', true).length === 4, - 'Four real ephemeral Pi children did not become active', - ); + try { + await waitFor( + () => composition.processBudget.activeCount === 8 + && composition.scheduler.getDiagnostics().activeChildPermits === 4 + && provider.activeCounts().child === 4 + && processIds(composition.tracked, 'child', true).length === 4, + 'Four real ephemeral Pi children did not become active', + ); + } catch (error) { + const childDiagnostics = composition.tracked + .filter(({ role }) => role === 'child') + .map(({ process }) => ({ + pid: process.processId ?? null, + running: process.isRunning, + diagnostic: process.stderrDiagnostic, + })); + throw new Error( + `${error instanceof Error ? error.message : String(error)}: ${JSON.stringify({ + snapshot: pressureSnapshot(composition, provider, writeLeases), + scheduler: composition.scheduler.getDiagnostics(), + providerRequests: providerRequestCounts(provider), + childDiagnostics, + })}`, + ); + } const active = pressureSnapshot(composition, provider, writeLeases); if (active.parentProcessIds.length !== 4 || active.childProcessIds.length !== 4 diff --git a/tests/unit/pi-provider-config.test.ts b/tests/unit/pi-provider-config.test.ts index acd3fe9..0e14df2 100644 --- a/tests/unit/pi-provider-config.test.ts +++ b/tests/unit/pi-provider-config.test.ts @@ -194,6 +194,21 @@ describe('Pi Provider catalog', () => { expect(await readFile(filePath, 'utf8')).toBe('existing-catalog\n'); }); + it('serializes concurrent writes to the shared managed catalog', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-provider-concurrent-')); + temporaryRoots.push(root); + const filePath = path.join(root, 'models.json'); + const catalog = buildPiProviderCatalog({ accounts: [account()] }); + + await Promise.all(Array.from({ length: 8 }, async () => await writePiProviderCatalog( + filePath, + catalog, + { accountId: 'account-one', modelId: 'gpt-5.4', thinkingLevel: 'off' }, + ))); + + expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual(catalog.modelsFile); + }); + it('fails closed when a non-local credential is unavailable', async () => { const provider = account(); const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!;