Skip to content

fix(engine): clear left_at when re-resolving a 1:1 DM - #302

Merged
khaliqgant merged 2 commits into
mainfrom
fix/dm-1-departed-participant-invariant
Jul 31, 2026
Merged

fix(engine): clear left_at when re-resolving a 1:1 DM#302
khaliqgant merged 2 commits into
mainfrom
fix/dm-1-departed-participant-invariant

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 31, 2026

Copy link
Copy Markdown
Member

What

A 1:1 DM conversation id is a pure function of (workspaceId, sorted agent pair), so it names the durable A↔B relationship rather than a conversation instance.

resolveConversation finds an existing 1:1 by joining dm_participants with left_at IS NULL. If a participant is ever marked departed the SELECT misses, the create branch runs, recomputes the same deterministic id, and re-inserts with onConflictDoNothing() — which no-ops against the existing conversation, channel, and participant rows, leaving left_at set.

The conversation then keeps resolving correctly while its roster says someone left. Nothing throws; the two just disagree from then on.

Identifier stability is not what breaks — the id never consults left_at. What breaks is the claim that the identifier names a relationship with fixed membership.

Why it matters outside this repo

That claim is load-bearing for the Ratify design partnership. Agent Relay has declared this lifecycle model to Identities AI as the basis on which Ratify delegation certificates are scoped to a DM — invariant DM-1 in the Agent Relay Resource Identifier Profile. It is stated as an invariant with a test rather than left as a code comment, so it cannot change silently later.

Change

The create branch clears left_at on conflict instead of no-opping. Reaching that branch for an id that already exists means the lookup missed, and for an existing 1:1 the only way it can miss is a departed participant — so re-resolution heals the roster.

Resolution semantics are otherwise unchanged. No current code path can set left_at for a 1:1: it is written only by the group-DM code (groupDm.ts). The test reaches that state directly so the invariant is guarded before some future path can reach it accidentally.

Tests

Four tests covering the model:

  • id derives from (workspace, sorted pair)
  • either sender resolves the same conversation
  • re-resolution creates no second conversation
  • DM-1: a 1:1 conversation never has a departed participant

The DM-1 test fails against the previous behaviour, which is the point — it pins the invariant, not the current behaviour.

Verification

  • npx turbo build && npx vitest run in packages/engine515/515
  • npx turbo lint — clean
  • Confirmed it is a real guard: with the one-line fix reverted, DM-1 is the only test in the engine suite whose result changes.

Review note

This touches production message-routing code (onConflictDoNothingonConflictDoUpdate in the 1:1 resolution path). It is one line and only fires in a branch unreachable today, but a reviewer who owns this file should confirm the conflict-target choice.

🤖 Generated with Claude Code

Review in cubic

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bdb4feb-9dcc-4c71-86b6-4d6f46dc84f9

📥 Commits

Reviewing files that changed from the base of the PR and between 9a745b4 and 7a88f50.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/engine/CHANGELOG.md
  • packages/engine/src/engine/__tests__/dm.test.ts
  • packages/engine/src/engine/dm.ts
📝 Walkthrough

Walkthrough

The DM participant conflict update clears leftAt for existing participants. New tests verify deterministic conversation identity, sender-order-independent reuse, duplicate prevention, and restoration of departed participants.

Changes

Deterministic DM behavior

Layer / File(s) Summary
Participant reactivation and invariant tests
packages/engine/src/engine/dm.ts, packages/engine/src/engine/__tests__/dm.test.ts
Participant conflicts clear leftAt. SQLite tests verify deterministic IDs, conversation reuse, unique participants, and restored rosters.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit guarding the DM trail,
Two names return when old rows fail.
The same path leads to the same room,
No duplicate burrows bloom.
Departed marks fade from view,
And both agents hop back through.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: clearing left_at when re-resolving a 1:1 DM.
Description check ✅ Passed The description directly explains the bug, the fix, the invariant, and the related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dm-1-departed-participant-invariant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a745b4adc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +98
}).onConflictDoUpdate(rejoin);
await db.insert(dmParticipants).values({
conversationId: deterministicConversationId,
agentId: toAgentId,
}).onConflictDoNothing();
}).onConflictDoUpdate(rejoin);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the DM rejoin fix to the changelogs

This changes user-visible behavior when a 1:1 participant has left_at set: the next send now restores that participant and resumes routing instead of leaving the roster departed. Neither the root changelog nor packages/engine/CHANGELOG.md records the fix, and the package changelog still has an unqualified [Unreleased] heading; add concise patch entries and raise that heading to [Unreleased - Patch] as required.

AGENTS.md reference: AGENTS.md:L39-L45

Useful? React with 👍 / 👎.

@khaliqgant

Copy link
Copy Markdown
Member Author

Addressed the Codex P1 in 2a8f23a — good catch, the AGENTS.md release-level rule was a repo convention I missed rather than an oversight I'd argue with.

  • Root CHANGELOG.md: bullet under the existing [Unreleased - Patch]### Fixed.
  • packages/engine/CHANGELOG.md: raised the bare [Unreleased] heading to [Unreleased - Patch] and added the package-level detail.

Patch is the right level: behaviour only changes in a state no current code path can reach (left_at is written solely by the group-DM path), and the conversation id is unchanged either way since it derives only from the workspace and the sorted agent pair.

CodeRabbit reported no actionable comments.

khaliqgant and others added 2 commits July 31, 2026 12:24
A 1:1 DM conversation id is a pure function of (workspaceId, sorted agent
pair), so it names the durable A<->B relationship rather than a conversation
instance. `resolveConversation` finds an existing 1:1 by joining
dm_participants with `left_at IS NULL`; if a participant is ever marked
departed the SELECT misses, the create branch runs, recomputes the same
deterministic id, and re-inserts with onConflictDoNothing() — which no-ops
against the existing rows and leaves `left_at` set.

The conversation then keeps resolving correctly while its roster says someone
left. Nothing throws; the two just disagree from then on. Identifier stability
is not what breaks — the id never consults `left_at` — what breaks is the
claim that the identifier names a relationship with fixed membership.

That claim is load-bearing outside this repo: Agent Relay has declared this
lifecycle model to Identities AI as the basis on which Ratify delegation
certificates are scoped to a DM (invariant DM-1 in the Agent Relay Resource
Identifier Profile). So it is stated as an invariant with a test rather than
left as a code comment.

The create branch now clears `left_at` on conflict instead of no-opping.
Reaching that branch for an id that already exists means the lookup missed,
and for an existing 1:1 the only way it can miss is a departed participant —
so re-resolution heals the roster. Resolution semantics are otherwise
unchanged, and no current code path can set `left_at` for a 1:1 (it is written
only by the group-DM code).

Adds four tests covering the model: id derives from (workspace, sorted pair);
either sender resolves the same conversation; re-resolution creates no second
conversation; and DM-1 itself. The DM-1 test fails against the previous
behaviour, which is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raises the engine changelog's pending heading to [Unreleased - Patch] per the
AGENTS.md release-level rule — the fix changes user-visible behaviour, since a
1:1 whose participant was marked departed now restores that participant on the
next send rather than resolving with a stale roster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant
khaliqgant force-pushed the fix/dm-1-departed-participant-invariant branch from 2a8f23a to 7a88f50 Compare July 31, 2026 10:25
@khaliqgant

Copy link
Copy Markdown
Member Author

Rebased onto main (was based on a stale checkout two commits behind, which is what made this conflict once the changelog edits landed). Now at 7a88f50, mergeable, CI green.

The changelog entries needed re-filing after the rebase: v6.3.0 had already been cut, so the bullet I'd added would have landed inside the released section. Both now sit under the pending heading:

  • root CHANGELOG.md: raised [Unreleased][Unreleased - Patch] with the user-facing bullet
  • packages/engine/CHANGELOG.md: same heading change, plus the package-level detail

Re-verified on the new base: turbo build + engine suite 524/524, turbo lint clean.

@khaliqgant
khaliqgant merged commit 161190b into main Jul 31, 2026
5 checks passed
@khaliqgant
khaliqgant deleted the fix/dm-1-departed-participant-invariant branch July 31, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant