diff --git a/packages/data-schemas/src/methods/agent.spec.ts b/packages/data-schemas/src/methods/agent.spec.ts index c48759ee72..47da924565 100644 --- a/packages/data-schemas/src/methods/agent.spec.ts +++ b/packages/data-schemas/src/methods/agent.spec.ts @@ -1980,6 +1980,40 @@ describe('Agent Methods', () => { } }); + test('should persist an update matching the newest version when the document has diverged from it', async () => { + const agentId = `agent_${uuidv4()}`; + const authorId = new mongoose.Types.ObjectId(); + + await createAgent({ + id: agentId, + provider: 'test', + model: 'test-model', + author: authorId, + tools: [], + }); + + /** Atomic-operator updates snapshot the pre-update state as the new version, so the + * document and `versions[versions.length - 1]` legitimately diverge. This is the + * same shape `addAgentResourceFile` produces when it attaches a file. */ + await updateAgent({ id: agentId }, { $addToSet: { tools: 'file_search' } }); + + const afterAdd = await getAgent({ id: agentId }); + expect(afterAdd!.tools).toEqual(['file_search']); + expect(afterAdd!.versions).toHaveLength(2); + expect((afterAdd!.versions![1] as VersionEntry).tools).toEqual([]); + + /** Removing the tool lands the document back on the newest version's content. That + * adds no history, but the removal itself must still be written. */ + const removed = await updateAgent({ id: agentId }, { tools: [] }); + + expect(removed!.tools).toEqual([]); + expect(removed!.versions).toHaveLength(2); + + const reloaded = await getAgent({ id: agentId }); + expect(reloaded!.tools).toEqual([]); + expect(reloaded!.versions).toHaveLength(2); + }); + test('should track updatedBy when a different user updates an agent', async () => { const agentId = `agent_${uuidv4()}`; const originalAuthor = new mongoose.Types.ObjectId(); diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index f2fe72d11a..0ed31cfe81 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -604,6 +604,9 @@ export function createAgentMethods( const Agent = mongoose.models.Agent as Model; const { updatingUserId = null, forceVersion = false, skipVersioning = false } = options; const mongoOptions = { new: true, upsert: false }; + /** Set when the update would snapshot a version identical to the newest one. The write + * still lands; only the `versions` entry is dropped. */ + let suppressedVersionEntry = false; const currentAgent = await Agent.findOne(searchParameter); if (currentAgent) { @@ -694,12 +697,14 @@ export function createAgentMethods( actionsHash, ); if (duplicateVersion && !forceVersion) { - const agentObj = currentAgent.toObject() as IAgent & { - version?: number; - versions?: unknown[]; - }; - agentObj.version = (versions as unknown[]).length; - return agentObj; + /** A snapshot identical to the newest version adds no history, but the update + * itself must still be applied. The document is not always equal to that + * version: `$push`/`$pull`/`$addToSet` updates snapshot the pre-update state, + * and `skipVersioning` writes snapshot nothing at all. Whenever the caller + * moves the document back onto the newest version's content — removing a tool + * `addAgentResourceFile` added, for instance — returning here dropped a real + * change and reported success. */ + suppressedVersionEntry = true; } } @@ -717,7 +722,7 @@ export function createAgentMethods( versionEntry.updatedBy = new mongoose.Types.ObjectId(updatingUserId); } - if (shouldCreateVersion) { + if (shouldCreateVersion && !suppressedVersionEntry) { updateData.$push = { ...(($push as Record) || {}), versions: versionEntry, @@ -725,11 +730,20 @@ export function createAgentMethods( } } - return (await Agent.findOneAndUpdate( + const updatedAgent = (await Agent.findOneAndUpdate( searchParameter, updateData, mongoOptions, ).lean()) as IAgent | null; + + /** Callers that create a version read `version` back from their own count of + * `versions`; a suppressed entry leaves that count unchanged, so report it here to + * keep the "no new version" signal these callers already relied on. */ + if (updatedAgent && suppressedVersionEntry) { + (updatedAgent as IAgent & { version?: number }).version = updatedAgent.versions?.length ?? 0; + } + + return updatedAgent; } /**