Skip to content

fix(chat): serialize rule updates - #3121

Open
elie222 wants to merge 3 commits into
mainfrom
codex/chat-bulk-rule-updates
Open

fix(chat): serialize rule updates#3121
elie222 wants to merge 3 commits into
mainfrom
codex/chat-bulk-rule-updates

Conversation

@elie222

@elie222 elie222 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Stack created with GitHub Stacks CLI • 4 of 4

Summary

  • Keep the existing updateRule tool as the single way to edit an existing rule.
  • Serialize concurrent updateRule executions within one assistant run so each update validates against the rule revision produced by the previous update.
  • Refresh the shared rule snapshot after each write, allowing the model to update several rules with separate ordinary tool calls.
  • Add no model-visible tool, schema, description, or system-prompt context.
  • Bump the observable chat pipeline version to 4.

Why

The assistant could already emit one updateRule call per requested rule. Multiple calls from the same model step could execute concurrently, however, so the first write advanced the account rules revision while a sibling call was still validating stale state. Runtime serialization fixes that race without adding a special-case bulk tool.

Testing

  • 12 focused rule-tool unit tests, including a regression test that holds the first write open and proves the second update does not read stale state
  • Targeted live-model eval verifies two existing-rule updates after reading the current rules
  • Ultracite and git diff checks

Risks

  • Concurrent updates within the same assistant run now wait for the prior updateRule call to finish; a slow first update delays later ones.
  • Serialization is scoped to updateRule calls in one run and does not coordinate different rule tool types or separate requests.

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
inbox-zero Ignored Ignored Preview Aug 1, 2026 6:53pm

@elie222
elie222 force-pushed the codex/chat-bulk-rule-updates branch from c863a5b to 497c23d Compare July 31, 2026 17:18

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

6 issues found and verified against the latest diff

You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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="apps/web/utils/rule/rule.ts">

<violation number="1" location="apps/web/utils/rule/rule.ts:236">
P2: Concurrent bulk/single-rule writes can produce incorrect duplicate history entries because the post-write query is not tied to rows affected by this write. Capturing the affected rows atomically or carrying an expected prior state through the write would keep history limited to this operation's changes.</violation>
</file>

<file name="apps/web/__tests__/eval/assistant-chat-rule-eval-test-utils.ts">

<violation number="1" location="apps/web/__tests__/eval/assistant-chat-rule-eval-test-utils.ts:22">
P3: mockSetRulesEnabled is added to the type and module mock builder but not to configureRuleMutationMocks, creating an inconsistency where the new mock lacks the default resolved value that all other rule mutation mocks receive through configureRuleMutationMocks.</violation>
</file>

<file name="apps/web/utils/ai/assistant/chat.ts">

<violation number="1" location="apps/web/utils/ai/assistant/chat.ts:270">
P3: The snapshot is loaded twice in the `writeResult.count !== changedRules.length` → `updateIsComplete === true` edge case. The first load verifies the update; the second loads the same data immediately after. Move the snapshot load outside the `if` block or early-return from the inner block to avoid the redundant query.</violation>
</file>

<file name="apps/web/utils/rule/rule.test.ts">

<violation number="1" location="apps/web/utils/rule/rule.test.ts:708">
P3: Add a test where `updateMany` returns `{ count: 0 }` (all rules already match the target state) and verify that `findMany` and `createRuleHistory` are not called.</violation>

<violation number="2" location="apps/web/utils/rule/rule.test.ts:709">
P3: Add an assertion verifying `prisma.rule.findMany` was called with the correct `where` (id `in` clause, `emailAccountId`, `enabled`) and `include: ruleHistoryRuleInclude`. Without it, a change that drops the `enabled` filter or the `include` in the `findMany` query would silently pass the test.</violation>

<violation number="3" location="apps/web/utils/rule/rule.test.ts:717">
P3: Add a test case for the enabling path (`enabled: true`) with non-empty rule IDs to verify the `updateMany` filter becomes `enabled: { not: true }` and `findMany` fetches rules with `enabled: true`.</violation>
</file>

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

Re-trigger cubic

Comment thread apps/web/utils/rule/rule.ts Outdated
export async function setRulesEnabled({
ruleIds,
emailAccountId,
enabled,

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: Concurrent bulk/single-rule writes can produce incorrect duplicate history entries because the post-write query is not tied to rows affected by this write. Capturing the affected rows atomically or carrying an expected prior state through the write would keep history limited to this operation's changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/rule/rule.ts, line 236:

<comment>Concurrent bulk/single-rule writes can produce incorrect duplicate history entries because the post-write query is not tied to rows affected by this write. Capturing the affected rows atomically or carrying an expected prior state through the write would keep history limited to this operation's changes.</comment>

<file context>
@@ -230,6 +230,44 @@ export function setRuleEnabled({
+export async function setRulesEnabled({
+  ruleIds,
+  emailAccountId,
+  enabled,
+}: {
+  ruleIds: string[];
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch on the old implementation. The bulk setRulesEnabled helper and its history path have now been removed entirely. The PR instead serializes the existing per-rule updateRule executions, so there is no bulk/single post-write history query or duplicate-history scenario left in the current diff.

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.

Commit d11e5ea addressed this comment by removing the bulk update/history path and serializing existing per-rule updates. This prevents concurrent writes from producing duplicate or unrelated history entries.

mockPartialUpdateRule: AnyMock;
mockUpdateRuleActions: AnyMock;
mockSetRuleEnabled?: AnyMock;
mockSetRulesEnabled?: AnyMock;

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: mockSetRulesEnabled is added to the type and module mock builder but not to configureRuleMutationMocks, creating an inconsistency where the new mock lacks the default resolved value that all other rule mutation mocks receive through configureRuleMutationMocks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/__tests__/eval/assistant-chat-rule-eval-test-utils.ts, line 22:

<comment>mockSetRulesEnabled is added to the type and module mock builder but not to configureRuleMutationMocks, creating an inconsistency where the new mock lacks the default resolved value that all other rule mutation mocks receive through configureRuleMutationMocks.</comment>

<file context>
@@ -19,6 +19,7 @@ type RuleMutationMocks = {
   mockPartialUpdateRule: AnyMock;
   mockUpdateRuleActions: AnyMock;
   mockSetRuleEnabled?: AnyMock;
+  mockSetRulesEnabled?: AnyMock;
 };
 
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The setRulesEnabled tool and all associated eval mock plumbing, including mockSetRulesEnabled, have now been removed. The current eval exercises two calls to the existing updateRule tool, so this inconsistency no longer exists in the PR.

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.

Commit d11e5ea addressed this comment by removing the setRulesEnabled mock plumbing entirely. The eval now uses existing updateRule calls instead, so the inconsistent default mock configuration no longer exists.

Comment thread apps/web/utils/ai/assistant/chat.ts Outdated
getLearnedPatterns: getLearnedPatternsTool(toolOptions),
createRule: createRuleTool(toolOptions),
updateRule: updateRuleTool(toolOptions),
setRulesEnabled: setRulesEnabledTool(toolOptions),

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: The snapshot is loaded twice in the writeResult.count !== changedRules.lengthupdateIsComplete === true edge case. The first load verifies the update; the second loads the same data immediately after. Move the snapshot load outside the if block or early-return from the inner block to avoid the redundant query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/ai/assistant/chat.ts, line 270:

<comment>The snapshot is loaded twice in the `writeResult.count !== changedRules.length` → `updateIsComplete === true` edge case. The first load verifies the update; the second loads the same data immediately after. Move the snapshot load outside the `if` block or early-return from the inner block to avoid the redundant query.</comment>

<file context>
@@ -266,6 +267,7 @@ export async function aiProcessAssistantChat({
     getLearnedPatterns: getLearnedPatternsTool(toolOptions),
     createRule: createRuleTool(toolOptions),
     updateRule: updateRuleTool(toolOptions),
+    setRulesEnabled: setRulesEnabledTool(toolOptions),
     deleteRule: deleteRuleTool(toolOptions),
     updateLearnedPatterns: updateLearnedPatternsTool(toolOptions),
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The bulk setRulesEnabled tool and its snapshot-verification path have now been removed. The only current change in chat.ts is the pipeline version bump; serialization lives inside the existing updateRule tool, which refreshes its snapshot once after each update.

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.

Commit d11e5ea addressed this comment by removing the bulk setRulesEnabled tool and its snapshot-verification path entirely. Rule updates now serialize through updateRule, which refreshes the snapshot once per update.

Comment thread apps/web/utils/rule/rule.test.ts Outdated
beforeEach(resetRuleMocks);

it("updates all enabled states in one write and records each history", async () => {
prisma.rule.updateMany.mockResolvedValue({ count: 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: Add a test where updateMany returns { count: 0 } (all rules already match the target state) and verify that findMany and createRuleHistory are not called.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/rule/rule.test.ts, line 708:

<comment>Add a test where `updateMany` returns `{ count: 0 }` (all rules already match the target state) and verify that `findMany` and `createRuleHistory` are not called.</comment>

<file context>
@@ -700,6 +701,56 @@ describe("rule history snapshots", () => {
+  beforeEach(resetRuleMocks);
+
+  it("updates all enabled states in one write and records each history", async () => {
+    prisma.rule.updateMany.mockResolvedValue({ count: 2 });
+    prisma.rule.findMany.mockResolvedValue([
+      { id: "rule-one", actions: [], group: null },
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The setRulesEnabled domain helper and its tests have now been removed from the PR, so the updateMany count-zero branch no longer exists. The replacement regression test covers concurrent calls through the existing updateRule tool.

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.

Commit d11e5ea addressed this comment by removing the setRulesEnabled helper and its count-zero branch entirely. A replacement regression test now covers concurrent updates through the existing updateRule tool.

Comment thread apps/web/utils/rule/rule.test.ts Outdated
const result = await setRulesEnabled({
ruleIds: ["rule-one", "rule-two"],
emailAccountId: EMAIL_ACCOUNT_ID,
enabled: false,

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: Add a test case for the enabling path (enabled: true) with non-empty rule IDs to verify the updateMany filter becomes enabled: { not: true } and findMany fetches rules with enabled: true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/rule/rule.test.ts, line 717:

<comment>Add a test case for the enabling path (`enabled: true`) with non-empty rule IDs to verify the `updateMany` filter becomes `enabled: { not: true }` and `findMany` fetches rules with `enabled: true`.</comment>

<file context>
@@ -700,6 +701,56 @@ describe("rule history snapshots", () => {
+    const result = await setRulesEnabled({
+      ruleIds: ["rule-one", "rule-two"],
+      emailAccountId: EMAIL_ACCOUNT_ID,
+      enabled: false,
+    });
+
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The setRulesEnabled helper and updateMany path have now been removed entirely. Enable and disable requests both continue through the existing updateRule tool, so this bulk-path test case is no longer applicable.

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.

Commit d11e5ea addressed this comment by removing the setRulesEnabled helper and bulk updateMany path entirely. Enable/disable operations now use updateRule, with coverage for multiple rule updates.

Comment thread apps/web/utils/rule/rule.test.ts Outdated

it("updates all enabled states in one write and records each history", async () => {
prisma.rule.updateMany.mockResolvedValue({ count: 2 });
prisma.rule.findMany.mockResolvedValue([

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: Add an assertion verifying prisma.rule.findMany was called with the correct where (id in clause, emailAccountId, enabled) and include: ruleHistoryRuleInclude. Without it, a change that drops the enabled filter or the include in the findMany query would silently pass the test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/rule/rule.test.ts, line 709:

<comment>Add an assertion verifying `prisma.rule.findMany` was called with the correct `where` (id `in` clause, `emailAccountId`, `enabled`) and `include: ruleHistoryRuleInclude`. Without it, a change that drops the `enabled` filter or the `include` in the `findMany` query would silently pass the test.</comment>

<file context>
@@ -700,6 +701,56 @@ describe("rule history snapshots", () => {
+
+  it("updates all enabled states in one write and records each history", async () => {
+    prisma.rule.updateMany.mockResolvedValue({ count: 2 });
+    prisma.rule.findMany.mockResolvedValue([
+      { id: "rule-one", actions: [], group: null },
+      { id: "rule-two", actions: [], group: null },
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The post-update findMany query belonged to the removed setRulesEnabled helper. That helper and its tests are no longer in the current diff; the PR now serializes ordinary updateRule calls instead, so this assertion is no longer applicable.

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.

Commit d11e5ea addressed this comment by removing the bulk setRulesEnabled helper and its post-update findMany query. Updates now use serialized ordinary updateRule calls, so the requested assertion is no longer applicable.

@elie222
elie222 force-pushed the codex/chat-bulk-rule-updates branch from 497c23d to 524eabe Compare July 31, 2026 17:32
@elie222 elie222 changed the title fix(chat): update rule states in bulk fix(chat): serialize rule updates Jul 31, 2026
@elie222
elie222 force-pushed the codex/chat-bulk-rule-updates branch from 524eabe to d11e5ea Compare August 1, 2026 17:35
@elie222
elie222 force-pushed the codex/chat-bulk-rule-updates branch from d11e5ea to c3d8a3d Compare August 1, 2026 18:53
Base automatically changed from codex/chat-tool-contracts to main August 6, 2026 09:32
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