-
Notifications
You must be signed in to change notification settings - Fork 0
fix(engine): reserve deterministic DM ids atomically #303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0d4e743
fix(engine): reserve deterministic DM ids atomically
4d1d4a5
fix(engine): return a coded 409 for pair-level DM reservation collisions
khaliqgant 950c05a
fix(engine): detect the DM pair conflict across driver error shapes
khaliqgant c56dbf6
docs(engine): correct the migration 0033 pre-flight audit
khaliqgant ef2a76f
docs: trim the DM reservation changelog entries to shipped impact
khaliqgant 6408197
add the pre-flight audit for ids that no longer match the derivation
khaliqgant 541f42e
cover self-DM reservation and orphaned-reservation recovery
khaliqgant 51923b9
fix(engine): reserve only two-participant 1:1 DMs in migration 0033
khaliqgant c1564ef
harden the audit stdin parser against wrangler output shape
khaliqgant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
.agentworkforce/trajectories/completed/2026-08/traj_pzkvekhsexoq/summary.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Trajectory: Implement atomic ResolveOrReserve for deterministic 1:1 DMs | ||
|
|
||
| > **Status:** ✅ Completed | ||
| > **Confidence:** 95% | ||
| > **Started:** August 2, 2026 at 08:11 AM | ||
| > **Completed:** August 2, 2026 at 08:26 AM | ||
|
|
||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| Added atomic 1:1 DM ResolveOrReserve reservations with migration backfill, fail-closed digest collision handling, and concurrent negative-control coverage. | ||
|
|
||
| **Approach:** Standard approach | ||
|
|
||
| --- | ||
|
|
||
| ## Key Decisions | ||
|
|
||
| ### Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows | ||
| - **Chose:** Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows | ||
| - **Reasoning:** The reservation must name workspace plus sorted pair in one conflict-detecting upsert. Reserving first prevents colliding requests from creating shared metadata before a winner exists, and a separate table can backfill existing DMs without changing nullable group-DM columns. | ||
|
|
||
| --- | ||
|
|
||
| ## Chapters | ||
|
|
||
| ### 1. Work | ||
| *Agent: default* | ||
|
|
||
| - Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows: Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows | ||
| - The reservation-first design passes the real concurrent send path and its negative control. Full engine build, lint, and 536-test suite are green; root/package-wide unrelated runner/load failures were isolated and the SDK timeout rerun passed 416/416. |
73 changes: 73 additions & 0 deletions
73
.agentworkforce/trajectories/completed/2026-08/traj_pzkvekhsexoq/trajectory.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| { | ||
| "id": "traj_pzkvekhsexoq", | ||
| "version": 1, | ||
| "task": { | ||
| "title": "Implement atomic ResolveOrReserve for deterministic 1:1 DMs" | ||
| }, | ||
| "status": "completed", | ||
| "startedAt": "2026-08-02T06:11:12.788Z", | ||
| "completedAt": "2026-08-02T06:26:48.981Z", | ||
| "agents": [ | ||
| { | ||
| "name": "default", | ||
| "role": "lead", | ||
| "joinedAt": "2026-08-02T06:13:21.452Z" | ||
| } | ||
| ], | ||
| "chapters": [ | ||
| { | ||
| "id": "chap_hdc34f62008i", | ||
| "title": "Work", | ||
| "agentName": "default", | ||
| "startedAt": "2026-08-02T06:13:21.452Z", | ||
| "endedAt": "2026-08-02T06:26:48.981Z", | ||
| "events": [ | ||
| { | ||
| "ts": 1785651201456, | ||
| "type": "decision", | ||
| "content": "Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows: Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows", | ||
| "raw": { | ||
| "question": "Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows", | ||
| "chosen": "Reserve deterministic 1:1 DM ids in a dedicated tuple table before creating channel or conversation rows", | ||
| "alternatives": [], | ||
| "reasoning": "The reservation must name workspace plus sorted pair in one conflict-detecting upsert. Reserving first prevents colliding requests from creating shared metadata before a winner exists, and a separate table can backfill existing DMs without changing nullable group-DM columns." | ||
| }, | ||
| "significance": "high" | ||
| }, | ||
| { | ||
| "ts": 1785652008647, | ||
| "type": "reflection", | ||
| "content": "The reservation-first design passes the real concurrent send path and its negative control. Full engine build, lint, and 536-test suite are green; root/package-wide unrelated runner/load failures were isolated and the SDK timeout rerun passed 416/416.", | ||
| "raw": { | ||
| "focalPoints": [ | ||
| "atomicity", | ||
| "negative-control", | ||
| "validation" | ||
| ], | ||
| "confidence": 0.95 | ||
| }, | ||
| "significance": "high", | ||
| "tags": [ | ||
| "focal:atomicity", | ||
| "focal:negative-control", | ||
| "focal:validation", | ||
| "confidence:0.95" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "retrospective": { | ||
| "summary": "Added atomic 1:1 DM ResolveOrReserve reservations with migration backfill, fail-closed digest collision handling, and concurrent negative-control coverage.", | ||
| "approach": "Standard approach", | ||
| "confidence": 0.95 | ||
| }, | ||
| "commits": [], | ||
| "filesChanged": [], | ||
| "projectId": "AgentWorkforce/relaycast", | ||
| "tags": [], | ||
| "_trace": { | ||
| "startRef": "21a5390929e16c21058e08256dcdc33434e8e108", | ||
| "endRef": "21a5390929e16c21058e08256dcdc33434e8e108" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| -- Bind every deterministic 1:1 DM id to the workspace and sorted participant | ||
| -- pair that derived it. The primary key is the atomic reservation seam: a | ||
| -- conflicting digest can never overwrite or alias another pair's conversation. | ||
| CREATE TABLE dm_conversation_reservations ( | ||
| conversation_id TEXT PRIMARY KEY, | ||
| workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, | ||
| participant_one_id TEXT NOT NULL, | ||
| participant_two_id TEXT NOT NULL, | ||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()), | ||
| CONSTRAINT dm_conversation_reservations_sorted_pair_check | ||
| CHECK (participant_one_id <= participant_two_id) | ||
| ); | ||
|
|
||
| CREATE UNIQUE INDEX dm_conversation_reservations_pair_unique | ||
| ON dm_conversation_reservations (workspace_id, participant_one_id, participant_two_id); | ||
|
|
||
| -- PRE-FLIGHT AUDIT (run BOTH read-only queries BEFORE applying this migration). | ||
| -- The backfill can abort for two independent reasons, and each needs its own | ||
| -- check. An earlier version of this comment shipped only query (a) and claimed an | ||
| -- empty result meant the migration would apply cleanly. That was wrong: a | ||
| -- duplicate pair passes (a) and still aborts the backfill on the pair-uniqueness | ||
| -- index. Raised in review of PR #303. | ||
| -- | ||
| -- (a) Roster shape. The backfill aborts if any single legacy 1:1 has a roster | ||
| -- that is not 1 or 2 distinct participants. That is deliberate - inventing a | ||
| -- tuple would bind a conversation to the wrong pair - but one corrupt row | ||
| -- blocks the whole deployment. | ||
| -- | ||
| -- SELECT dc.id, dc.workspace_id, COUNT(DISTINCT dp.agent_id) AS participants | ||
| -- FROM dm_conversations dc | ||
| -- LEFT JOIN dm_participants dp ON dp.conversation_id = dc.id | ||
| -- WHERE dc.dm_type = '1:1' | ||
| -- GROUP BY dc.id, dc.workspace_id | ||
| -- HAVING participants NOT BETWEEN 1 AND 2; | ||
| -- | ||
| -- (b) Duplicate pairs. Two DISTINCT legacy 1:1 conversations in one workspace | ||
| -- that resolve to the same sorted pair both satisfy (a), but only one can be | ||
| -- reserved - the second violates dm_conversation_reservations_pair_unique and | ||
| -- aborts the migration. This is exactly what a pre-deterministic id scheme | ||
| -- leaves behind, so it is the likelier of the two in practice. | ||
| -- | ||
| -- SELECT workspace_id, participant_one_id, participant_two_id, | ||
| -- COUNT(*) AS conversations | ||
| -- FROM ( | ||
| -- SELECT dc.workspace_id, | ||
| -- MIN(dp.agent_id) AS participant_one_id, | ||
| -- MAX(dp.agent_id) AS participant_two_id | ||
| -- FROM dm_conversations dc | ||
| -- JOIN dm_participants dp ON dp.conversation_id = dc.id | ||
| -- WHERE dc.dm_type = '1:1' | ||
| -- GROUP BY dc.id, dc.workspace_id | ||
| -- HAVING COUNT(DISTINCT dp.agent_id) BETWEEN 1 AND 2 | ||
| -- ) | ||
| -- GROUP BY workspace_id, participant_one_id, participant_two_id | ||
| -- HAVING conversations > 1; | ||
| -- | ||
| -- (c) Ids that do not match the CURRENT derivation. This one cannot be written | ||
| -- in SQL - it needs SHA-256, which SQLite does not have - and it is the check | ||
| -- that matters most operationally, because it is the only failure that is | ||
| -- INVISIBLE AT MIGRATION TIME. (a) and (b) abort the migration loudly. (c) | ||
| -- lets it succeed, and then every subsequent DM between that pair returns 409 | ||
| -- forever, because the backfill reserved the pair under an id the send path | ||
| -- will never re-derive. | ||
| -- | ||
| -- Run: node scripts/audit-dm-reservations.mjs --sqlite <path> | ||
| -- D1: wrangler d1 execute <DB> --json --command "<see script header>" \ | ||
| -- | node scripts/audit-dm-reservations.mjs --stdin | ||
| -- | ||
| -- That script also re-runs (a) and (b), so it is the single command to trust. | ||
| -- | ||
| -- All three clean means this migration will apply AND no existing pair will start | ||
| -- failing afterwards. Remediate anything any of them returns - for (b) that means | ||
| -- deciding which conversation survives, since the reservation can only bind one; | ||
| -- for (c) it means re-keying the conversation to the derived id, or seeding its | ||
| -- reservation under the derived id, before deploying the code that reserves. | ||
|
|
||
| -- Backfill ONLY conversations with exactly two distinct participants. | ||
| -- | ||
| -- A one-row roster is ambiguous and MUST NOT be reserved. It looks like a | ||
| -- self-DM, but `dm_participants.agent_id` cascades on agent deletion, so a | ||
| -- perfectly ordinary two-party 1:1 collapses to a single row the moment one | ||
| -- participant's agent is deleted - while its id still encodes the ORIGINAL pair. | ||
| -- | ||
| -- Reading those as (X, X) is wrong twice over. Several orphans belonging to the | ||
| -- same surviving agent all collapse to the same (workspace, X, X) tuple and | ||
| -- collide on the pair-uniqueness index, aborting the migration; and any that | ||
| -- survived would reserve a self-DM tuple against an id no derivation produces, | ||
| -- so that agent's next self-DM would 409 forever. | ||
| -- | ||
| -- This is not hypothetical. An earlier version of this backfill used | ||
| -- MIN/MAX over 1-2 participants; audited against production it produced 4 | ||
| -- colliding pair groups and 30 mismatched ids, all of them orphaned two-party | ||
| -- conversations, and would have failed the deployment. Restricted to exactly two | ||
| -- participants the same data yields zero findings across 3425 conversations. | ||
| -- | ||
| -- Skipping is safe rather than merely convenient. An unreserved conversation is | ||
| -- in exactly the state every conversation was in before this migration: the | ||
| -- first send through the reservation path claims it, and because a genuine | ||
| -- self-DM's id already equals its derivation, that claim adopts the existing | ||
| -- conversation instead of creating a second one. Orphaned two-party rows are | ||
| -- simply never re-derived, so they stay readable and inert. | ||
| -- | ||
| -- Malformed rosters (zero, or more than two) are skipped for the same reason. | ||
| -- The earlier version aborted the whole migration on them; skipping avoids | ||
| -- inventing a tuple just as effectively without blocking a deployment, and any | ||
| -- future send still goes through the reservation path. | ||
| INSERT INTO dm_conversation_reservations ( | ||
| conversation_id, | ||
| workspace_id, | ||
| participant_one_id, | ||
| participant_two_id, | ||
| created_at | ||
| ) | ||
| SELECT | ||
| dc.id, | ||
| dc.workspace_id, | ||
| MIN(dp.agent_id), | ||
| MAX(dp.agent_id), | ||
| dc.created_at | ||
| FROM dm_conversations dc | ||
| JOIN dm_participants dp ON dp.conversation_id = dc.id | ||
| WHERE dc.dm_type = '1:1' | ||
| GROUP BY dc.id, dc.workspace_id, dc.created_at | ||
| HAVING COUNT(DISTINCT dp.agent_id) = 2; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: AgentWorkforce/relaycast
Length of output: 373
🏁 Script executed:
Repository: AgentWorkforce/relaycast
Length of output: 50380
Add a composite FK that enforces reserved DM pairs match
dmParticipantsrows.dm_conversation_reservations.participantOneIdandparticipantTwoIdare not linked todm_participants, whiledmParticipants.agentIdhas a foreign key toagents.id. Add a SQL check/query constraint that each sorted pair exists indm_participantsfor the sameworkspace_id, so deleted participants are not silently reclaimed as available valid reservation rows.🤖 Prompt for AI Agents