From c47806d6cc3264065ca6c1d225809431ff218c7d Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 9 Jul 2026 17:27:10 +0000 Subject: [PATCH] Make managed resource binding upsert atomic Co-Authored-By: Paperclip --- server/src/__tests__/built-in-agents.test.ts | 38 ++++++++++++++++++++ server/src/services/built-in-agents.ts | 34 ++++++++---------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/server/src/__tests__/built-in-agents.test.ts b/server/src/__tests__/built-in-agents.test.ts index 6f0676661fd..3f74b37a4dc 100644 --- a/server/src/__tests__/built-in-agents.test.ts +++ b/server/src/__tests__/built-in-agents.test.ts @@ -509,6 +509,44 @@ describeEmbeddedPostgres("built-in agents", () => { expect(coachGrantKeys).not.toContain("skills:create"); }); + it("recreates missing managed resource bindings idempotently during concurrent reconcile", async () => { + const companyId = await seedCompany({ requireApproval: false }); + await agentService(db).create(companyId, { + name: "CEO", + role: "ceo", + status: "idle", + adapterType: "codex_local", + adapterConfig: { model: "gpt-5.4" }, + runtimeConfig: {}, + permissions: {}, + }); + const builtIns = builtInAgentService(db); + await builtIns.ensure(companyId, "reflection-coach"); + await db.delete(builtInManagedResources).where(eq(builtInManagedResources.companyId, companyId)); + + const states = await Promise.all([ + builtIns.ensure(companyId, "reflection-coach"), + builtIns.ensure(companyId, "reflection-coach"), + ]); + + expect(states).toHaveLength(2); + for (const state of states) { + expect(state.resources.map((resource) => [resource.resourceKind, resource.stockStatus])).toEqual([ + ["instructions", "stock_current"], + ["skill", "stock_current"], + ["routine", "stock_current"], + ]); + } + const bindings = await db + .select() + .from(builtInManagedResources) + .where(eq(builtInManagedResources.companyId, companyId)); + expect(bindings).toHaveLength(3); + expect(new Set(bindings.map((binding) => + `${binding.bundleKey}:${binding.resourceKind}:${binding.resourceKey}` + )).size).toBe(3); + }); + it("preserves new-agent approval gates during automatic Reflection Coach provisioning", async () => { const companyId = await seedCompany({ requireApproval: true }); const mutationPolicy = { diff --git a/server/src/services/built-in-agents.ts b/server/src/services/built-in-agents.ts index 8bd0b905ef2..a530183a9f6 100644 --- a/server/src/services/built-in-agents.ts +++ b/server/src/services/built-in-agents.ts @@ -662,29 +662,25 @@ export function builtInAgentService(db: Db) { stockHash: string; defaultsJson: Record; }) { - const existing = await getManagedResourceBinding( - input.companyId, - input.bundleKey, - input.resourceKind, - input.resourceKey, - ); - if (existing) { - return db - .update(builtInManagedResources) - .set({ + const now = new Date(); + return db + .insert(builtInManagedResources) + .values(input) + .onConflictDoUpdate({ + target: [ + builtInManagedResources.companyId, + builtInManagedResources.bundleKey, + builtInManagedResources.resourceKind, + builtInManagedResources.resourceKey, + ], + set: { resourceId: input.resourceId, stockVersion: input.stockVersion, stockHash: input.stockHash, defaultsJson: input.defaultsJson, - updatedAt: new Date(), - }) - .where(eq(builtInManagedResources.id, existing.id)) - .returning() - .then((rows) => rows[0] ?? null); - } - return db - .insert(builtInManagedResources) - .values(input) + updatedAt: now, + }, + }) .returning() .then((rows) => rows[0] ?? null); }