Skip to content

fix(agent): keep tool modes on their tools across API and concurrent edits - #7829

Closed
j15z wants to merge 1 commit into
codex/agent-tool-permission-modefrom
codex/agent-tool-mode-sync-fixes
Closed

j15z wants to merge 1 commit into
codex/agent-tool-permission-modefrom
codex/agent-tool-mode-sync-fixes

Conversation

@j15z

@j15z j15z commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #7538. Fixes two problems found while QA-ing variable tool permission modes.

API and Chat edits lose the inactive value of a basic/advanced pair. An edit replaces the whole tools list, so switching a pair by sending only one side dropped the other side's stored value. This affected Permission Mode (sending only usageControlExpression dropped the fixed usageControl) and nested tool params (sending only Jira's manualProjectId dropped the picked projectId). The edit engine now matches each rewritten tool to the one it replaced and:

  • keeps the side the caller left out,
  • selects the mode from the side the caller sent (both sides, or a pair resent unchanged, keeps the current mode),
  • leaves tools identical to their previous entry untouched.

Concurrent editors can put a tool mode on the wrong tool. Tool-scoped mode keys are positional (${toolIndex}:…), and a toggle racing another editor's removal or reorder could persist at an index its tool no longer holds, so the next tool added there silently inherited Variable mode with an empty expression and runs failed. Now:

  • a tool-scoped toggle carries a toolRef (subblock, index, tool identity) and the realtime server skips it when that position holds a different tool; a skipped toggle is confirmed to its sender but not broadcast,
  • editors ignore a broadcast toggle for a tool they have already moved or removed,
  • removing or reordering tools always saves the list and its modes in one operation, including when none of the editor's own modes need to move.

A toggle made at the same moment as another editor's removal can still be dropped, but consistently: every editor and the saved state agree, and it can no longer land on another tool. toolRef is optional, so older clients and servers keep the previous behavior.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • Unit tests for the edit-engine mode selection and carry-over, the shared tool identity guard, realtime persistence of guarded toggles, and skipping the broadcast of a stale toggle.
  • type-check for sim, realtime, and realtime-protocol; Biome; check:api-validation, check:boundaries, and check:realtime-prune pass.
  • Manual, v2 API: on a Jira + Wikipedia agent, switching to manual ID / Variable and back keeps both sides; a read-and-resend round trip leaves modes unchanged; reordering moves modes with their tools. Before this change the picked project and the fixed none were dropped.
  • Manual, two editors: one tab toggles tool 3 while the other removes tool 1 at fixed offsets. Before: 5 of 5 rounds lost or misplaced the toggle, 2 left it on an empty position. After: 7 rounds, none misplaced; the server logged the skipped stale toggles; with the toggle clearly first it stays on its tool.

Reviewers: applySuppliedToolModes in lib/workflows/editing/tool-modes.ts and isToolInputRefCurrent in packages/realtime-protocol/src/tool-input.ts.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

No screenshots attached.

🤖 Generated with Claude Code

…edits

- API and Chat edits that switch a basic/advanced pair by sending one side
  keep the side they left out, for Permission Mode and nested tool params
  such as Jira's project picker and manual ID. The side sent selects the
  mode, and a pair resent unchanged keeps its mode.
- A tool-scoped mode toggle names its tool. The realtime server refuses it
  when that position no longer holds the tool, editors ignore a broadcast
  toggle for a tool they moved or removed, and removing or reordering tools
  always saves the list and its modes together, so a concurrent toggle can
  no longer land on another tool or an empty position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@j15z
j15z requested a review from a team as a code owner September 14, 2026 23:12
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 14, 2026 11:12pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

The PR is not safe to merge until duplicate-tool identity, rewritten-tool matching, and rejected optimistic-toggle reconciliation are addressed.

Findings

  1. P1 Duplicate tools share identity
  2. P1 Reordering can mix tool values
  3. P1 Rejected toggles remain local

Summary

This PR preserves inactive basic/advanced tool values during serialized edits and adds guarded, atomic persistence for positional tool modes during concurrent edits.

  • Adds shared tool-reference identity and stale-reference validation across clients and the realtime server.
  • Carries omitted canonical-pair and permission-mode values from matched prior tools.
  • Persists reordered tool lists together with their reindexed canonical modes.
  • Adds coverage for edit carry-over, persistence guards, and stale-broadcast suppression.

Diagram

sequenceDiagram
  participant A as Editor A
  participant B as Editor B
  participant R as Realtime server
  participant DB as Workflow database
  A->>A: Optimistically apply tool-mode toggle
  B->>R: Persist tool reorder/removal with modes
  R->>DB: Atomically replace tools and modes
  R-->>A: Broadcast reorder/removal
  A->>R: Send positional toggle with toolRef
  R->>DB: Compare persisted position and identity
  alt Reference is current
    DB-->>R: Apply mode
    R-->>B: Broadcast toggle
    R-->>A: Confirm operation
  else Reference is stale
    DB-->>R: Reject mode
    R-->>A: Confirm without rollback
  end
Loading

Reviews (1) · Last reviewed commit: "fix(agent): keep tool modes on their too..."

Comment on lines +11 to +13
const TOOL_IDENTITY_FIELDS = ['type', 'operation', 'toolId', 'customToolId'] as const

const TOOL_IDENTITY_PARAM_FIELDS = ['serverId', 'toolName', 'workflowId'] as const

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.

P1 Duplicate tools share identity

Two Jira tools can have the same type and operation while differing only by parameters such as projectId. Because those parameters are excluded from this identity and stored tools have no stable per-instance ID, swapping the two tools still satisfies the identity check. An in-flight positional mode toggle can therefore be accepted for the other Jira instance and assign its mode to the wrong tool.

Comment on lines +69 to +75
const previousIndexByIndex = new Map<number, number>()
if (originalTools) {
for (const [previousIndex, index] of matchRewrittenTools(originalTools, tools)) {
previousIndexByIndex.set(index, previousIndex)
}
}

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.

P1 Reordering can mix tool values

When two tools of the same type are both edited and reordered, neither may match its old entry by full content. The fallback matcher then pairs them by type and position, and this code uses those matches to restore omitted basic, advanced, or permission-mode values. As a result, an edited tool can receive inactive values from the other tool and persist a corrupted configuration.

Comment on lines +579 to +595
if (persisted?.applied !== false) {
const broadcastData = {
operation,
target,
payload,
timestamp: operationTimestamp,
senderId: socket.id,
userId: session.userId,
userName: session.userName,
metadata: {
workflowId,
operationId: generateId(),
},
}

socket.to(workflowId).emit('workflow-operation', broadcastData)
socket.to(workflowId).emit('workflow-operation', broadcastData)
}

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.

P1 Rejected toggles remain local

The sender applies a mode toggle optimistically before queueing it. When the server rejects that toggle as stale, this branch suppresses the broadcast but still confirms the operation; confirmation only removes it from the queue and does not roll back or reload state. If the competing move or removal was already applied locally before the toggle was queued, no later update corrects the optimistic value, leaving that editor inconsistent with persisted state.

@j15z j15z closed this Sep 15, 2026
@waleedlatif1
waleedlatif1 deleted the codex/agent-tool-mode-sync-fixes branch September 15, 2026 00:20
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