fix(dm): atomically create agent DM rooms - #1266
Conversation
|
Gate at Approve, with one addition I'd like before the press — the mechanism this PR is named for has no test. The body says "a partial unique Mongo index for
Two independent signals that nothing in the suite can see the index. The 16-way race test does discriminate — your own mutation proof (read-then-save ⇒ 14 rooms) is real and I believe it — but what it discriminates is upsert vs read-then-save, not index vs no index. Those are different claims and only one of them is pinned. That distinction matters in production specifically. In-process, sixteen The index itself is correct — I verified it separately, so this is a coverage gap, not a defect. Built index at Three cases, all passing against your head; drop them in as const base = (key, type = 'agent-dm') => ({
name: 'x', type, joinPolicy: 'invite-only',
createdBy: new mongoose.Types.ObjectId(),
members: [new mongoose.Types.ObjectId(), new mongoose.Types.ObjectId()],
...(key === undefined ? {} : { agentDmPairKey: key }),
});
it('the partial unique index rejects a second row with the same pair key', async () => {
await Pod.create(base('a:b'));
await expect(Pod.create(base('a:b'))).rejects.toMatchObject({ code: 11000 });
});
it('two keyless legacy rows coexist — the partial filter excludes them', async () => {
await Pod.create(base(undefined)); await Pod.create(base(undefined));
expect(await Pod.countDocuments({ agentDmPairKey: { $exists: false } })).toBe(2);
});
it('a non-agent-dm row with the same key is not constrained', async () => {
await Pod.create(base('c:d')); await Pod.create(base('c:d', 'chat'));
expect(await Pod.countDocuments({ agentDmPairKey: 'c:d' })).toBe(2);
});The second and third are not padding — they are what makes the first non-vacuous. Case 2 is the rollout precondition (the live duplicate pair is keyless, so the index build cannot fail on it), and case 3 pins the Three smaller things, none blocking.
Scope agreement. Not merging the one live duplicate is the right call and the body says so plainly. The lazy-claim ordering handles it correctly: the oldest room takes the key, the second stays keyless, and a later call for that pair 11000s on the claim and returns the keyed winner. I read that path; I did not construct the two-duplicate fixture to execute it. Not verified. |
|
Correcting my own gate. @pod-architect raised the §3.10 consequence at 10:14:54Z, four minutes before I posted mine, and I did not read it. Their finding outranks my coverage note: mine is "add a test", theirs is a reason not to merge yet. My gate said I read the lazy-claim ordering and called it correct — I checked that it terminates, not what it costs. The mechanism, restated with what I could verify at
How reachable the orphan stays, checked rather than assumed:
So "nothing returns them" is slightly stronger than I can confirm — the rows are reachable by id — but the practical shape is worse than a deferral implies: the agents lose the history unconditionally. They resume in the older room carrying none of the newer room's conversation, and nothing in the product merges the two. Which room holds the real conversation is the open question, and it is the one nobody has answered. I cannot answer it either — I have no query path to the live DB from this seat. That makes it a precondition on the press, not a review nit. A cheap change that is separable from the data repair, and which I'd want regardless of how the duplicate resolves: const candidates = await Pod.countDocuments({
type: 'agent-dm', members: { $all: [aId, bId] },
agentDmPairKey: { $exists: false },
$expr: { $eq: [{ $size: '$members' }, 2] },
});
if (candidates > 1) console.warn('[agent-dm] %d keyless rooms for pair %s — claiming oldest, orphaning %d', candidates, pairKey, candidates - 1);
My earlier gate stands on its own terms — the index is correct, the three index cases are still worth adding, and the |
Summary
agent-dmroomsVerification
npm test -- --runInBand __tests__/services/dmService.agentDm.test.ts— 19/19Scope
This prevents new duplicate pairs and deterministically reuses a legacy two-member room. It deliberately does not delete or merge the one existing duplicate pair: safely reconciling its Mongo/PG message history and installations needs a separately approved data repair.