Skip to content

fix(engine): reserve deterministic DM ids atomically - #303

Merged
khaliqgant merged 9 commits into
mainfrom
agent/atomic-dm-resolve-or-reserve
Aug 2, 2026
Merged

fix(engine): reserve deterministic DM ids atomically#303
khaliqgant merged 9 commits into
mainfrom
agent/atomic-dm-resolve-or-reserve

Conversation

@kjgbot

@kjgbot kjgbot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What changed

  • add a durable dm_conversation_reservations tuple table keyed by deterministic conversation ID
  • reserve (conversation_id, workspace_id, sorted participant pair) with one conditional upsert before creating DM metadata
  • accept identical re-resolution and fail closed with dm_conversation_id_collision when the ID is already bound to a different tuple
  • backfill existing 1:1 and self-DM reservations in migration 0033, aborting on malformed legacy rosters
  • relabel the digest helper as derivation-only and keep all resolution in the real sendDm path
  • add a Promise.allSettled test that launches two conflicting real sends concurrently and proves exactly one wins

Why

The previous resolver derived a stable ID, then inserted the conversation with ON CONFLICT DO NOTHING. Because the stored conversation row did not include the participant tuple, a digest collision could silently resolve a second pair onto the first pair's conversation. A read-before-write check would still race; the tuple has to be claimed by a database uniqueness boundary and checked in the same upsert statement.

Impact

Normal first resolution and idempotent re-resolution retain the same deterministic IDs. A conflicting tuple now receives a hard 409 dm_conversation_id_collision instead of aliasing another conversation. Existing 1:1/self-DM rows are backfilled during migration.

This PR needs a dm.ts owner review before merge.

Validation

  • npx turbo build --env-mode=loose: 9/9 tasks passed
  • focused concurrent collision test: 1/1 passed
  • npx vitest run packages/engine: 50 files, 536 tests passed
  • npx turbo lint --filter=@relaycast/engine --env-mode=loose: 3/3 tasks passed
  • SDK package isolation rerun: 22 files, 416 tests passed

Negative control: temporarily removed the conversation_id primary-key uniqueness constraint from migration 0033, rebuilt, and reran the concurrent test. With no matching database uniqueness boundary, both conflict-target upserts rejected and the test failed exactly as intended:

FAIL  packages/engine/src/engine/__tests__/dm.test.ts > 1:1 DM conversation identity > atomically rejects one of two concurrent pairs whose digests collide
AssertionError: expected [] to have a length of 1 but got +0

- Expected
+ Received

- 1
+ 0

Test Files  1 failed (1)
Tests  1 failed | 4 skipped (5)

Additional repository-wide diagnostics:

  • root npx vitest run: 107 files / 1,373 tests passed; 11 unrelated failures because root invocation lacks the React jsdom environment and types globals
  • npx turbo test --env-mode=loose: two SDK tests timed out at 5 seconds under nine-package parallel load; both passed in the isolated SDK rerun above

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9aa3bf6-6d4e-4a88-9e79-72fac2f71d1a

📥 Commits

Reviewing files that changed from the base of the PR and between 6408197 and c1564ef.

📒 Files selected for processing (3)
  • packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql
  • packages/engine/src/engine/__tests__/dm.test.ts
  • scripts/audit-dm-reservations.mjs

📝 Walkthrough

Walkthrough

The PR adds workspace-scoped reservations for deterministic 1:1 DM conversation IDs. It backfills eligible conversations, rejects conflicting bindings with a 409 error, restores participants, audits migration data, and adds collision and concurrency tests.

Changes

Deterministic 1:1 DM reservations

Layer / File(s) Summary
Reservation schema and migration
packages/engine/src/db/schema.ts, packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql, scripts/audit-dm-reservations.mjs
Adds reservation constraints, canonical participant ordering, workspace-pair uniqueness, migration audits, safe backfill logic, and audit reporting.
Reservation-first DM resolution and validation
packages/engine/src/engine/dm.ts, packages/engine/src/engine/__tests__/dm.test.ts
Reserves conversation IDs before metadata creation, handles both collision shapes as dm_conversation_id_collision, restores participants, and tests wrapped errors and digest collisions.
Release and implementation records
CHANGELOG.md, packages/engine/CHANGELOG.md, README.md, openapi.yaml, .agentworkforce/trajectories/completed/2026-08/traj_pzkvekhsexoq/*
Documents the 409 response, migration behavior, collision handling, and completed implementation results.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant sendDm
  participant ReservationTable
  participant ConversationRecords

  Client->>sendDm: Send a 1:1 DM
  sendDm->>ReservationTable: Reserve workspace and participant pair
  ReservationTable-->>sendDm: Return reservation or 409 collision
  sendDm->>ConversationRecords: Create or reuse conversation metadata
  ConversationRecords-->>Client: Return resolved conversation
Loading

Possibly related PRs

Suggested labels: size:XL

Poem

I’m a rabbit guarding pairs in line,
With sorted paws and IDs aligned.
One digest wins; collisions stop,
Metadata follows the reserved hop.
Tests race twice, then calmly cheer—
Atomic DMs are safe and clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: atomic reservation of deterministic DM conversation IDs.
Description check ✅ Passed The description directly explains the reservation design, collision handling, migration, tests, and validation for the changeset.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/atomic-dm-resolve-or-reserve

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

scripts/audit-dm-reservations.mjs

Parsing error: /scripts/audit-dm-reservations.mjs was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.


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.

@khaliqgant
khaliqgant marked this pull request as ready for review August 2, 2026 07:17

@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: 0d4e743dc3

ℹ️ 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 +79 to +80
'dm_conversation_id_collision',
409,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the new DM collision response

When a deterministic digest collision occurs, POST /dm now returns the new 409 dm_conversation_id_collision error, but this commit leaves the endpoint's OpenAPI responses declaring only 201 and does not update README.md. Add the new response to openapi.yaml and synchronize the README so generated API documentation and clients describe the observable failure mode, as required for API behavior changes.

AGENTS.md reference: AGENTS.md:L34-L36

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql (1)

17-37: 🧹 Nitpick | 🔵 Trivial

Run a pre-flight audit before applying this migration in production.

The backfill aborts the entire migration if any single legacy 1:1 conversation has zero or more than two distinct participants. This is intentional fail-closed behavior, but it means one corrupt legacy row blocks the whole deployment. Run a read-only audit query against production data before applying migration 0033, to find and remediate malformed rosters ahead of the cutover.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql`
around lines 17 - 37, Before applying migration 0033, run a read-only audit
grouped by 1:1 conversation to identify rows whose distinct participant count is
not between 1 and 2, using the same dm_conversations and dm_participants
criteria as the backfill. Remediate each malformed roster, then rerun the audit
to confirm no invalid conversations remain before executing the migration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/db/schema.ts`:
- Around line 606-632: Add a composite foreign-key constraint to
dmConversationReservations linking workspaceId, participantOneId, and
participantTwoId to the corresponding sorted-pair key on dmParticipants,
ensuring each reservation matches an existing pair in the same workspace. Add or
reuse the required unique composite key on dmParticipants, and preserve the
existing sorted-pair check and uniqueness constraint.

---

Nitpick comments:
In `@packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql`:
- Around line 17-37: Before applying migration 0033, run a read-only audit
grouped by 1:1 conversation to identify rows whose distinct participant count is
not between 1 and 2, using the same dm_conversations and dm_participants
criteria as the backfill. Remediate each malformed roster, then rerun the audit
to confirm no invalid conversations remain before executing the migration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 66b6c072-65c4-4b64-a9a8-3ea3d0c39813

📥 Commits

Reviewing files that changed from the base of the PR and between 21a5390 and 0d4e743.

📒 Files selected for processing (8)
  • .agentworkforce/trajectories/completed/2026-08/traj_pzkvekhsexoq/summary.md
  • .agentworkforce/trajectories/completed/2026-08/traj_pzkvekhsexoq/trajectory.json
  • CHANGELOG.md
  • packages/engine/CHANGELOG.md
  • packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql
  • packages/engine/src/db/schema.ts
  • packages/engine/src/engine/__tests__/dm.test.ts
  • packages/engine/src/engine/dm.ts

Comment on lines +606 to +632
// ============================================
// 1:1 DM Conversation Reservations
// ============================================
export const dmConversationReservations = sqliteTable(
'dm_conversation_reservations',
{
conversationId: text('conversation_id').primaryKey(),
workspaceId: text('workspace_id')
.notNull()
.references(() => workspaces.id, { onDelete: 'cascade' }),
participantOneId: text('participant_one_id').notNull(),
participantTwoId: text('participant_two_id').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
},
(table) => [
check(
'dm_conversation_reservations_sorted_pair_check',
sql`${table.participantOneId} <= ${table.participantTwoId}`,
),
uniqueIndex('dm_conversation_reservations_pair_unique').on(
table.workspaceId,
table.participantOneId,
table.participantTwoId,
),
],
);

Copy link
Copy Markdown

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:

#!/bin/bash
# Description: Check whether dm_participants (and other agent-referencing tables) enforce an FK to agents, to compare with dmConversationReservations.
ast-grep run --pattern 'agentId: text($_).notNull().references($_)' --lang typescript packages/engine/src/db/schema.ts
rg -n -B3 -A3 'agentId' packages/engine/src/db/schema.ts | rg -n -B3 -A3 'dm_participants|dmParticipants'

Repository: AgentWorkforce/relaycast

Length of output: 373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== schema agents and dm_participants/dmConversationReservations sections ==="
sed -n '520,660p' packages/engine/src/db/schema.ts

echo
echo "=== FK references to agents.id in schema ==="
rg -n '\.references\(\(\) => agents\.id|agentId[^:]*text\([^)]*\)\.notNull\(\)(\.[^;}]+)?\.references|agent_id' packages/engine/src/db/schema.ts

echo
echo "=== dm_participants table usage / delete paths ==="
rg -n 'dm_participants|dmParticipants|agentId|deleteFrom|where.*agentId|agentId.*where' packages/engine/src -g '*.ts'

Repository: AgentWorkforce/relaycast

Length of output: 50380


Add a composite FK that enforces reserved DM pairs match dmParticipants rows.

dm_conversation_reservations.participantOneId and participantTwoId are not linked to dm_participants, while dmParticipants.agentId has a foreign key to agents.id. Add a SQL check/query constraint that each sorted pair exists in dm_participants for the same workspace_id, so deleted participants are not silently reclaimed as available valid reservation rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/db/schema.ts` around lines 606 - 632, Add a composite
foreign-key constraint to dmConversationReservations linking workspaceId,
participantOneId, and participantTwoId to the corresponding sorted-pair key on
dmParticipants, ensuring each reservation matches an existing pair in the same
workspace. Add or reuse the required unique composite key on dmParticipants, and
preserve the existing sorted-pair check and uniqueness constraint.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/engine/src/engine/dm.ts Outdated
Comment thread packages/engine/src/engine/__tests__/dm.test.ts
Addresses PR #303 review feedback.

The conditional upsert names conversation_id as its ON CONFLICT target, which
SQLite limits to one. The pair_unique index on
(workspace_id, participant_one_id, participant_two_id) is therefore not a
conflict target, so the second collision shape -- same pair reserved under a
DIFFERENT conversation_id -- raised an uncaught SQLITE_CONSTRAINT_UNIQUE and
surfaced as a 500 rather than the controlled 409.

It is reachable rather than theoretical: migration 0033 backfills whatever
`dc.id` a legacy 1:1 already had, without requiring it to equal the current
derivation, so the pair can end up reserved under an id the next send will not
re-derive. An out-of-band write does the same.

Failing closed was already true; failing closed with the DOCUMENTED code was
not, and a caller cannot distinguish a refused collision from an engine fault.
The insert now narrows that constraint failure -- matched on this table only, so
an unrelated constraint error is never laundered into a tidy 409 -- and raises
the same coded error as the primary-key path.

Regression test added, and its negative control confirmed: with the handler
disabled, exactly one test flips and reports
"expected 'SQLITE_CONSTRAINT_UNIQUE' to be 'dm_conversation_id_collision'".

Also from review:
- openapi.yaml and README.md document the 409 and both shapes that produce it,
  per the AGENTS.md docs-hygiene rule for API behaviour changes.
- Migration 0033 carries the read-only pre-flight audit query operators should
  run before applying it, since the fail-closed backfill aborts the deployment
  on a single malformed legacy roster. The query is verified to flag 0- and
  3-participant 1:1s while ignoring valid and group rows.

NOT taken: the suggestion to add a composite FK binding reservation
participants to dm_participants. The reservation is written BEFORE any roster
row exists -- that ordering is the point of the seam, since the reservation must
win before conversation state is created -- so such an FK would reject every new
conversation.

engine 526/526, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member

Review feedback addressed in 4d1d4a5. Three taken, one declined with a reason.

Taken

1. cubic P2 — pair-unique collision surfaced as a 500 (dm.ts:66). Valid, and it was a real defect rather than a theoretical one. I reproduced it first:

AssertionError: expected a coded collision, got: UNIQUE constraint failed:
dm_conversation_reservations.workspace_id, ...participant_one_id, ...participant_two_id
expected 'SQLITE_CONSTRAINT_UNIQUE' to be 'dm_conversation_id_collision'

SQLite accepts one ON CONFLICT target, so the pair-unique index was unhandled and the second collision shape — same pair under a different conversation_id — escaped as a raw constraint error. Reachable exactly as described: migration 0033 backfills whatever dc.id a legacy 1:1 already had, without requiring it to equal the current derivation, so the pair can be reserved under an id the next send will not re-derive.

Failing closed was already true; failing closed with the documented code was not, and a caller cannot tell a refused collision from an engine fault. The insert now narrows that constraint failure — matched on this table only, so an unrelated constraint error is never laundered into a tidy 409 — and raises the same coded error as the primary-key path.

Regression test added, plus its negative control: with the handler disabled, exactly one test flips, with the message above.

2. Codex P2 — document the 409. Done in openapi.yaml and README.md, per the AGENTS.md docs-hygiene rule, covering both shapes that produce it and noting it is not retryable. openapi.yaml parses clean.

3. CodeRabbit nitpick — pre-flight audit. Agreed, and rather than only warning, migration 0033 now carries the read-only audit query operators should run before applying it. I verified the query behaves: it flags 0- and 3-participant 1:1s and ignores valid and group rows.

Declined

CodeRabbit major — composite FK from reservations to dm_participants (schema.ts:632). Not taken, because it would break every new conversation.

The reservation is written before any roster row exists — resolveOrReserveConversation runs first, then channels, dmConversations, and only then dmParticipants. That ordering is the entire point of the seam: the reservation has to win before conversation state is created, so exactly one tuple can survive a collision. An FK requiring a matching dm_participants pair would reject the reservation on the first insert for every genuinely new DM.

The invariant the suggestion is reaching for — a reservation should not outlive its roster — is real, but a foreign key in this direction cannot express it. It would need to be a deletion-path concern instead. Happy to open a follow-up if you want that tracked.

Verification

npx turbo build --env-mode=loose     9/9 tasks
engine suite                         526/526 (49 files)
npx turbo lint --filter=@relaycast/engine   3/3 tasks
git diff --check                     clean

Note the count differs from the 536 quoted earlier in this PR — that figure came from a different checkout scope. 526/526 is what a clean tree at this branch produces, and I confirmed 49 tracked test files equal 49 present.

Still needs a dm.ts owner review; not merging.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/engine/src/engine/dm.ts Outdated
Comment thread packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql Outdated
Addresses the P1 and P3 raised on the previous review round.

P1. The first version of this handler matched `.code` and `.message` on the
top-level error only. That passes against better-sqlite3, which is what the test
suite runs, and would have regressed in the hosted engine, which runs Cloudflare
D1 via drizzle-orm/d1 -- D1 prefixes its message with "D1_ERROR: " and drizzle
may re-wrap the driver error under `.cause` rather than surfacing it at the top
level.

This engine has already shipped that exact regression once: agent.ts gained
isUniqueConstraintError in PR #193 after clean 409 handling became an uncaught
500 against D1 for the same reason, and observerToken.ts documents the trap. I
reproduced the shape rather than reasoning about it, and reused the established
cycle-safe walk instead of inventing a third variant.

Detection now requires BOTH conditions somewhere in the cause chain: a unique
violation, and a reference to dm_conversation_reservations. They are tracked
independently because a wrapper can carry the code while only the wrapped cause
carries the message. Keeping the table condition is what stops a FOREIGN KEY or
NOT NULL failure on the same insert from being reported as a participant-pair
conflict, which would be a tidy 409 that says something untrue.

The walk is iterative with a WeakSet of visited objects, breaking on any
revisit rather than a direct self-reference, so a multi-step cycle cannot blow
the stack and turn the check meant to prevent a 500 into one.

Tests assert all four shapes directly, plus the cases that must NOT match and a
cyclic chain. Negative control: restricting detection to the top level fails the
driver-shape test alone.

P3. The concurrency test is renamed to "rejects the losing pair when two
colliding sends interleave". Both real sendDm paths are launched before either
is awaited, but better-sqlite3 is a single synchronous connection, so they
serialize and there is no genuine database contention. It proves the collision
branch and the ordering of the reservation seam; it does not prove multi-writer
atomicity against a networked engine. The constraint is what provides atomicity
and the DO NOTHING control shows it is load-bearing. Named for what it
demonstrates.

engine 529/529, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member

Second round addressed in 950c05a.

cubic P1 — hosted collisions could still 500 · valid, fixed

This was the important one and it was right. My previous fix matched .code and .message on the top-level error only — which passes against better-sqlite3, the driver the test suite runs, and would have regressed in the hosted engine on D1, where the message is prefixed D1_ERROR: and drizzle may re-wrap the driver error under .cause.

Worth saying plainly: this engine has already shipped that exact regression once. agent.ts gained isUniqueConstraintError in PR #193 after clean 409 handling became an uncaught 500 against D1 for the same reason, and observerToken.ts documents the trap in detail. I had written a third variant of a detector this repo has twice learned to write properly. Fixed by reusing the established cycle-safe walk.

Detection now requires both conditions somewhere in the cause chain — a unique violation, and a reference to dm_conversation_reservations — tracked independently, because a wrapper can carry the code while only the wrapped cause carries the message. Keeping the table condition is what stops a FOREIGN KEY or NOT NULL failure on the same insert from being reported as a participant-pair conflict, which would be a tidy 409 that says something untrue.

The walk is iterative with a WeakSet of visited objects and breaks on any revisit, not just a direct self-reference — a multi-step cycle (A -> B -> A) would otherwise blow the stack and turn the check meant to prevent a 500 into one.

Four shapes now asserted directly (better-sqlite3; D1 top-level; D1 re-wrapped under .cause; code and message split across the chain), plus the cases that must not match and a cyclic chain. Negative control: restricting detection back to the top level fails the driver-shape test alone.

cubic P3 — the test says concurrency, the driver serializes · valid, renamed

Correct, and I had already flagged the same caveat to our partner rather than let the word carry more weight than it earns. Renamed to “rejects the losing pair when two colliding sends interleave.”

Both real sendDm paths are launched before either is awaited, so the production path is exercised with a forced collision — but better-sqlite3 is a single synchronous connection, so they serialize and there is no genuine DB contention. It proves the collision branch and the ordering of the reservation seam. It does not prove multi-writer atomicity against a networked engine; the constraint provides that, and the DO NOTHING control shows the constraint is load-bearing. The test docblock now says exactly this.

Codex P2 — document the 409 · already addressed in 4d1d4a5

That comment re-anchored to the previous commit. openapi.yaml declares 409 under /dm and README.md:414 documents it; both landed before this round.

CodeRabbit — composite FK · still declined

Unchanged reasoning: the reservation is written before any dm_participants row exists, and that ordering is the point of the seam. Such an FK would reject every new conversation. Detail in my previous comment.

Verification

npx turbo build --env-mode=loose            9/9 tasks
engine suite                                529/529
npx turbo lint --filter=@relaycast/engine   3/3 tasks
git diff --check                            clean

Still needs a dm.ts owner review; not merging.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/engine/CHANGELOG.md Outdated
Raised in review of PR #303, and the reviewer is right.

The audit I added last round checked roster shape only, then claimed an empty
result meant the migration would apply cleanly. It does not. Two DISTINCT legacy
1:1 conversations in one workspace that resolve to the same sorted pair both
pass the shape check and still abort the backfill on
dm_conversation_reservations_pair_unique. Reproduced before fixing:

  shape audit flags: NOTHING (claims clean)
  backfill: ABORTED - SQLITE_CONSTRAINT_UNIQUE

That is the likelier of the two failures in practice, since duplicate pairs are
exactly what a pre-deterministic id scheme leaves behind - so the audit was
silent on the case operators are most likely to hit.

The comment now carries both queries, says both must be empty, and notes that
remediating (b) means deciding which conversation survives, because the
reservation can only bind one. Both queries verified against a seeded database:
shape catches a 3-participant row, duplicate-pair catches the colliding pair, and
valid 1:1s and group DMs are ignored by both.

Documentation only; no schema or code change.

engine 529/529, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member

Third round in c56dbf6.

cubic P3 — migration audit was incomplete · valid, fixed

Good catch, and it was an overclaim in my own words. The audit I added checked roster shape only, then asserted an empty result meant the migration would apply cleanly. It does not. I reproduced it before fixing:

shape audit flags: NOTHING (claims clean)
backfill: ABORTED — SQLITE_CONSTRAINT_UNIQUE

Two distinct legacy 1:1 conversations in one workspace resolving to the same sorted pair both pass the shape check and still abort on dm_conversation_reservations_pair_unique. That is also the likelier of the two failures in practice — duplicate pairs are exactly what a pre-deterministic id scheme leaves behind — so the audit was silent on the case operators are most likely to hit.

The comment now carries both queries, states that both must be empty, and notes that remediating (b) means deciding which conversation survives, since the reservation can only bind one. Both queries verified against a seeded DB: (a) catches a 3-participant roster, (b) catches the colliding pair, and valid 1:1s plus group DMs are ignored by both.

Documentation only — no schema or code change.

Re-anchored comments from earlier rounds

Three comments re-posted against the new line numbers but carry their original text; all were addressed before this round:

  • Codex P2, document the 409 — done in 4d1d4a5. openapi.yaml declares 409 under /dm; README.md:414 documents it.
  • cubic P3, test labelled concurrency — done in 950c05a. Renamed to “rejects the losing pair when two colliding sends interleave”, with a docblock stating that better-sqlite3 serializes the sends, so it proves the collision branch and the reservation ordering but not multi-writer atomicity.
  • CodeRabbit, composite FK — still declined. The reservation is written before any dm_participants row exists; that ordering is the point of the seam, so the FK would reject every new conversation.

Verification

npx turbo build --env-mode=loose            9/9 tasks
engine suite                                529/529
npx turbo lint --filter=@relaycast/engine   3/3 tasks
git diff --check                            clean

Still needs a dm.ts owner review; not merging.

AGENTS.md:45-46 asks for one short impact-first bullet per user-visible change
and explicitly excludes PR links, internal review notes, and implementation
backstory. My entries had grown to three bullets in the engine changelog and two
at the root, describing which index was violated, which error shapes are walked,
and citing PR #193 -- none of which a reader of release notes needs.

Consolidated to one bullet each, stated as impact: a collision returns
409 dm_conversation_id_collision on every driver rather than aliasing another
pair's conversation or failing with an uncaught database error. The engine entry
keeps the migration reference, which AGENTS.md:43 does ask package changelogs to
carry, and points at the migration header for the pre-flight audit.

The regression backstory stays where it is useful: in the code comments beside
the detector it explains.

engine 529/529, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member

Fourth round in ef2a76f.

cubic P3 — changelog carried backstory · valid, fixed

Correct, and I checked the rule rather than taking the quote on faith: AGENTS.md:45-46 does say “Keep entries concise and impact-first: one short bullet per user-visible change” and “Omit PR links, internal review notes, test-only work, and implementation backstory unless they explain shipped impact.”

My entries had grown to three bullets in the engine changelog and two at the root, describing which index was violated, which error shapes get walked, and citing PR #193 — none of which a reader of release notes needs. Consolidated to one bullet each, stated as impact:

sendDm atomically reserves each deterministic 1:1 conversation ID for its workspace and sorted participant pair, so a collision returns 409 dm_conversation_id_collision on every driver instead of resolving to another pair’s conversation or failing with an uncaught database error. Migration 0033 adds dm_conversation_reservations and backfills existing 1:1 DMs; see the migration header for the pre-flight audit to run before applying it.

The migration reference stays because AGENTS.md:43 asks package changelogs to carry API and migration detail. The regression backstory stays where it earns its place — in the code comments beside the detector it explains.

Remaining comments are re-anchors from earlier rounds

All three were addressed before this commit and carry their original text against new line numbers:

Comment Status
Codex P2, document the 409 Done in 4d1d4a5openapi.yaml declares 409 under /dm, README.md:414 documents it
cubic P3, test labelled concurrency Done in 950c05a — renamed, with a docblock stating better-sqlite3 serializes the sends
CodeRabbit, composite FK Declined — the reservation is written before any dm_participants row exists, so the FK would reject every new conversation

Verification

npx turbo build --env-mode=loose            9/9 tasks
engine suite                                529/529
npx turbo lint --filter=@relaycast/engine   3/3 tasks
git diff --check                            clean

Five review findings taken across four rounds, one declined with reasoning. Still needs a dm.ts owner review; not merging.

The two SQL audits in the 0033 header cover the failures that ABORT the
migration. They do not cover the one that does not: a 1:1 conversation whose id
is not what deriveDmPairKey produces today.

That case is quiet and it is the dangerous one. The backfill reserves whatever
`dc.id` the conversation already had, so the pair ends up bound to an id the send
path will never re-derive. The migration succeeds, nothing complains, and then
every subsequent DM between that pair returns 409 forever. Before this PR the
same rows would have silently created a duplicate conversation instead, so the
change converts a quiet data problem into a loud one - which is right, but it
means latent mess surfaces at deploy time.

It cannot be written in SQL because SQLite has no SHA-256, so it is a script.
It runs all three checks, reads either a SQLite file or `wrangler d1 execute
--json` on stdin so both deployment shapes are covered, and is read-only.

Git history says the derivation never changed - the only edit swapped node
crypto for web crypto with a byte-identical input string. This exists because
"stable in git history" and "zero rows in production disagree" are different
claims and only the second is evidence.

Verified against a seeded database: flags a 3-participant roster, a duplicate
pair, and two mismatched ids, while leaving a correct pair, a self-DM, the same
pair in a different workspace, and a group DM alone. Clean fixtures exit 0.

Docs and tooling only; no schema or engine change.

engine 529/529, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member

Deployment risk — closed the gap I found in my own audit

Pushed 6408197. Not from a review comment; from asking what could actually break on rollout.

The three ways migration 0033 can go wrong are not equally visible:

Failure When you find out
(a) Malformed roster Migration aborts. Immediately.
(b) Duplicate pair in a workspace Migration aborts. Immediately.
(c) Conversation id ≠ current derivation Migration succeeds — then every DM between that pair returns 409 forever.

The two SQL audits I added earlier only cover (a) and (b) — the loud ones. (c) was uncovered, and it is the dangerous one. The backfill reserves whatever dc.id a conversation already had; if that is not what deriveDmPairKey produces today, the send path derives a different id for the same pair, hits pair_unique, and fails closed permanently.

It cannot be written in SQL — SQLite has no SHA-256 — so scripts/audit-dm-reservations.mjs runs all three. It reads either a SQLite file or wrangler d1 execute --json on stdin, so self-hosted and hosted are both covered, and it is read-only.

node scripts/audit-dm-reservations.mjs --sqlite /path/to/relay.db

How likely is (c)? Low. Git history says the derivation never changed — the only edit swapped node crypto for web crypto with a byte-identical input string, and I checked the diff rather than assuming. But "stable in git history" and "zero rows in production disagree" are different claims, and only the second is evidence. Now it is one command to get.

Verified against a seeded database: flags a 3-participant roster, a duplicate pair, and two mismatched ids, while leaving a correct pair, a self-DM, the same pair in a different workspace, and a group DM alone. Clean fixtures exit 0.

One behaviour change worth stating explicitly for whoever reviews this

Before this PR, a case-(c) row would have silently created a duplicate conversation. After it, that same row returns a hard 409. That is the right direction — aliasing another pairs conversation is the bug this PR exists to prevent — but it means latent data problems surface at deploy time rather than staying hidden. Run the audit first and there are no surprises.

Deployment order

Migration 0033 must land before the code that reserves. The reservation path depends on the table existing.

engine 529/529, build 9/9, lint 3/3, clean diff. Still needs a dm.ts owner; not merging.

Two properties this change depends on in production that nothing asserted.

Self-DM. A self-DM has one roster row, so the reservation stores the same agent
as both participants. That satisfies the sorted-pair CHECK only because the
comparison is non-strict. `@self` is a documented request shape, so a stricter
constraint would have broken a live feature with no test to catch it.

Orphaned reservation. The reservation and the conversation/roster inserts are
NOT one transaction, so a crash between them leaves a reservation with no
conversation. Recovery depends on the retry presenting the identical tuple,
which the conditional upsert accepts. I had asserted that was self-healing when
reasoning about deployment risk; now it is tested. If re-resolution ever stopped
accepting an identical tuple, one mid-write crash would lock that pair out of
DMs permanently and nothing else in this suite would notice.

Both pass today. Tests only; no behaviour change.

engine 531/531, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/audit-dm-reservations.mjs (1)

49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard against derivation drift with more than a comment.

deriveConversationId duplicates the SHA-256 derivation that deriveDmPairKey implements in dm.ts. The comment on Line 48 states it "Must stay byte-identical," but nothing enforces that beyond the comment itself. If either implementation changes independently, this script will silently pass or fail conversations incorrectly, and the failure would only surface as unexplained mismatches in production audits.

Add a small cross-check (for example, a unit test that imports or calls both derivations with the same fixed input vector and asserts equality) so a future edit to either implementation fails a test instead of silently drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/audit-dm-reservations.mjs` around lines 49 - 56, Add a focused test
that invokes both deriveConversationId and deriveDmPairKey with the same fixed
workspace and agent IDs, then asserts their derived values are equal. Keep the
existing derivation unchanged and ensure the test fails if either implementation
drifts from the other.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/audit-dm-reservations.mjs`:
- Around line 114-123: Update the roster-building loop around conversations and
introduce droppedRows tracking for every row missing id, workspaceId, or agentId
instead of silently continuing. Add a report finding for droppedRows.length
alongside malformed, duplicates, and mismatched, and include that finding in the
exit-code calculation so skipped rows fail the audit.

---

Nitpick comments:
In `@scripts/audit-dm-reservations.mjs`:
- Around line 49-56: Add a focused test that invokes both deriveConversationId
and deriveDmPairKey with the same fixed workspace and agent IDs, then asserts
their derived values are equal. Keep the existing derivation unchanged and
ensure the test fails if either implementation drifts from the other.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8635c665-6bcd-4b2a-9d6b-d96d53e187ad

📥 Commits

Reviewing files that changed from the base of the PR and between 4d1d4a5 and 6408197.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/engine/CHANGELOG.md
  • packages/engine/src/db/migrations/0033_dm_conversation_reservations.sql
  • packages/engine/src/engine/__tests__/dm.test.ts
  • packages/engine/src/engine/dm.ts
  • scripts/audit-dm-reservations.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
  • CHANGELOG.md
  • packages/engine/src/engine/dm.ts
  • packages/engine/CHANGELOG.md

Comment on lines +114 to +123
// Group the roster per conversation.
const conversations = new Map();
for (const row of rows) {
const id = row.id ?? row.conversation_id;
const workspaceId = row.workspace_id;
const agentId = row.agent_id;
if (!id || !workspaceId || !agentId) continue;
if (!conversations.has(id)) conversations.set(id, { id, workspaceId, agents: new Set() });
conversations.get(id).agents.add(agentId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Report rows dropped for missing fields instead of silently skipping them.

The continue on Line 120 discards any row missing id, workspaceId, or agentId without recording it. This script exists to catch anomalies before an irreversible migration. A row with a corrupt agent_id is exactly the kind of anomaly checks (a)/(b)/(c) are designed to catch, but this loop removes it before the roster is even built.

Two failure modes follow:

  • A conversation with a mix of valid and corrupt participant rows can end up with an apparently valid 1-2 person roster and pass check (a) undetected.
  • A conversation whose only rows are corrupt never enters the conversations map, so it is invisible to every subsequent check.

Track and report skipped rows so the audit surfaces them instead of silently hiding them.

🛠️ Proposed fix to surface dropped rows
+const droppedRows = [];
 for (const row of rows) {
   const id = row.id ?? row.conversation_id;
   const workspaceId = row.workspace_id;
   const agentId = row.agent_id;
-  if (!id || !workspaceId || !agentId) continue;
+  if (!id || !workspaceId || !agentId) {
+    droppedRows.push(row);
+    continue;
+  }
   if (!conversations.has(id)) conversations.set(id, { id, workspaceId, agents: new Set() });
   conversations.get(id).agents.add(agentId);
 }

Then add a section that reports droppedRows.length as a finding (and includes it in the exit-code calculation) alongside malformed, duplicates, and mismatched.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/audit-dm-reservations.mjs` around lines 114 - 123, Update the
roster-building loop around conversations and introduce droppedRows tracking for
every row missing id, workspaceId, or agentId instead of silently continuing.
Add a report finding for droppedRows.length alongside malformed, duplicates, and
mismatched, and include that finding in the exit-code calculation so skipped
rows fail the audit.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/audit-dm-reservations.mjs">

<violation number="1" location="scripts/audit-dm-reservations.mjs:120">
P2: The audit can certify incomplete stdin data as clean because rows missing required fields are silently skipped. Rejecting invalid rows, rather than continuing, prevents a partial export from hiding migration findings.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const id = row.id ?? row.conversation_id;
const workspaceId = row.workspace_id;
const agentId = row.agent_id;
if (!id || !workspaceId || !agentId) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The audit can certify incomplete stdin data as clean because rows missing required fields are silently skipped. Rejecting invalid rows, rather than continuing, prevents a partial export from hiding migration findings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/audit-dm-reservations.mjs, line 120:

<comment>The audit can certify incomplete stdin data as clean because rows missing required fields are silently skipped. Rejecting invalid rows, rather than continuing, prevents a partial export from hiding migration findings.</comment>

<file context>
@@ -0,0 +1,192 @@
+  const id = row.id ?? row.conversation_id;
+  const workspaceId = row.workspace_id;
+  const agentId = row.agent_id;
+  if (!id || !workspaceId || !agentId) continue;
+  if (!conversations.has(id)) conversations.set(id, { id, workspaceId, agents: new Set() });
+  conversations.get(id).agents.add(agentId);
</file context>

The audit found this against real production data, and as written the
migration WOULD HAVE FAILED THE DEPLOYMENT.

Production has 3562 1:1 conversations. The old backfill used MIN/MAX over
1-2 distinct participants, which produced 4 colliding pair groups and 30
mismatched ids. The colliding groups abort the migration on the
pair-uniqueness index; the mismatches would have made those agents 409 on
every subsequent DM.

None of the 30 were self-DMs. `dm_participants.agent_id` references
`agents.id` ON DELETE CASCADE, so deleting an agent silently removes its
participant rows and an ordinary two-party 1:1 collapses to a ONE-ROW ROSTER
while its id still encodes the ORIGINAL pair. The backfill read those as
(X, X). Several orphans belonging to the same surviving agent then collapse
onto the same tuple and collide.

I checked the obvious explanation first and it was wrong: no current agent in
those workspaces reproduces the stored id, so the peer agent rows are gone
entirely, not merely departed. `left_at` is null on the survivors.

The backfill now reserves only conversations with exactly two distinct
participants. Against the same production data: 3425 reserved, zero
duplicates, zero mismatches, 137 skipped (107 genuine self-DMs, 30 orphans).

Skipping is safe rather than 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 forking a second one. That property is now a test; without it every
pre-migration self-DM would silently split on first use and its history would
disappear from the user's view.

Malformed rosters (zero or more than two) are skipped for the same reason. The
old version aborted the entire migration on them, which avoids inventing a
tuple no more effectively than skipping does, and blocks a deployment.

Two-party conversations, which are the ones that matter, were never at risk:
3425 of them, zero mismatches. The derivation has been stable exactly as git
history suggested.

engine 532/532, build 9/9, lint 3/3, git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/audit-dm-reservations.mjs">

<violation number="1" location="scripts/audit-dm-reservations.mjs:160">
P3: Zero-participant malformed conversations are never reported even though this branch classifies them as malformed: the inner-join input and grouping logic make an empty roster impossible. Preserve conversation rows with no participants in the audit input, or remove the unreachable zero-roster classification so the audit does not imply coverage it lacks.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

participants: agents.length,
kind: agents.length === 1 ? (looksLikeSelfDm ? 'self-DM' : 'orphaned two-party') : 'malformed',
});
if (agents.length === 0 || agents.length > 2) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Zero-participant malformed conversations are never reported even though this branch classifies them as malformed: the inner-join input and grouping logic make an empty roster impossible. Preserve conversation rows with no participants in the audit input, or remove the unreachable zero-roster classification so the audit does not imply coverage it lacks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/audit-dm-reservations.mjs, line 160:

<comment>Zero-participant malformed conversations are never reported even though this branch classifies them as malformed: the inner-join input and grouping logic make an empty roster impossible. Preserve conversation rows with no participants in the audit input, or remove the unreachable zero-roster classification so the audit does not imply coverage it lacks.</comment>

<file context>
@@ -126,16 +133,37 @@ const malformed = [];
+      participants: agents.length,
+      kind: agents.length === 1 ? (looksLikeSelfDm ? 'self-DM' : 'orphaned two-party') : 'malformed',
+    });
+    if (agents.length === 0 || agents.length > 2) {
+      malformed.push({ id: conv.id, workspaceId: conv.workspaceId, participants: agents.length });
+    }
</file context>

The pre-flight failed once mid-session with 'could not find a results array',
on the same query that had worked minutes earlier. A deploy gate that fails
intermittently, with an error that says nothing about what it received, is worse
than no gate.

It now skips any preamble, searches for the first array whose members carry
agent_id and workspace_id rather than enumerating envelope shapes, treats an
empty result set as legitimate, and on failure prints what it actually got plus
the aliases it expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/audit-dm-reservations.mjs">

<violation number="1" location="scripts/audit-dm-reservations.mjs:86">
P2: A banner or warning containing `[` or `{` still breaks the deploy-gate parser, despite this change claiming to tolerate preambles; parse candidate JSON starts until a valid envelope is found rather than trusting the first delimiter.</violation>

<violation number="2" location="scripts/audit-dm-reservations.mjs:119">
P2: An empty bare-array result is reported as invalid input instead of a clean audit, so deployments with no 1:1 DMs can fail the pre-flight gate when Wrangler emits `[]`; treat an empty top-level array as an empty result set too.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const rows = find(parsed);
if (!rows) {
// An empty result set is legitimate: a deployment with no 1:1 DMs at all.
if (JSON.stringify(parsed).includes('"results":[]')) return [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: An empty bare-array result is reported as invalid input instead of a clean audit, so deployments with no 1:1 DMs can fail the pre-flight gate when Wrangler emits []; treat an empty top-level array as an empty result set too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/audit-dm-reservations.mjs, line 119:

<comment>An empty bare-array result is reported as invalid input instead of a clean audit, so deployments with no 1:1 DMs can fail the pre-flight gate when Wrangler emits `[]`; treat an empty top-level array as an empty result set too.</comment>

<file context>
@@ -79,17 +79,50 @@ async function loadRows(argv) {
+    const rows = find(parsed);
+    if (!rows) {
+      // An empty result set is legitimate: a deployment with no 1:1 DMs at all.
+      if (JSON.stringify(parsed).includes('"results":[]')) return [];
+      usage(
+        'could not find a row array containing agent_id and workspace_id.\n'
</file context>
Suggested change
if (JSON.stringify(parsed).includes('"results":[]')) return [];
if ((Array.isArray(parsed) && parsed.length === 0)
|| JSON.stringify(parsed).includes('"results":[]')) return [];

// depending on version and TTY attachment, and an operator running this as a
// deploy gate should not have to care. A brittle parser here fails exactly
// when it matters most.
const start = raw.search(/[[{]/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A banner or warning containing [ or { still breaks the deploy-gate parser, despite this change claiming to tolerate preambles; parse candidate JSON starts until a valid envelope is found rather than trusting the first delimiter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/audit-dm-reservations.mjs, line 86:

<comment>A banner or warning containing `[` or `{` still breaks the deploy-gate parser, despite this change claiming to tolerate preambles; parse candidate JSON starts until a valid envelope is found rather than trusting the first delimiter.</comment>

<file context>
@@ -79,17 +79,50 @@ async function loadRows(argv) {
+    // depending on version and TTY attachment, and an operator running this as a
+    // deploy gate should not have to care. A brittle parser here fails exactly
+    // when it matters most.
+    const start = raw.search(/[[{]/);
+    if (start === -1) usage(`stdin contained no JSON. First 200 chars:\n${raw.slice(0, 200)}`);
+
</file context>

@khaliqgant
khaliqgant merged commit 7b397e5 into main Aug 2, 2026
5 checks passed
@khaliqgant
khaliqgant deleted the agent/atomic-dm-resolve-or-reserve branch August 2, 2026 13:58
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.

2 participants