Skip to content

Comments

testing pr 217 001#10

Open
zboyles wants to merge 33 commits intotest-pr-217-001from
01-24-add_auto-continuation_for_tool_approval_workflow
Open

testing pr 217 001#10
zboyles wants to merge 33 commits intotest-pr-217-001from
01-24-add_auto-continuation_for_tool_approval_workflow

Conversation

@zboyles
Copy link
Owner

@zboyles zboyles commented Jan 28, 2026

No description provided.

Seth Raphael and others added 30 commits January 14, 2026 21:31
## Problem

When streaming multiple tool calls, new tool calls were corrupting existing ones. The bug was in `combineUIMessages`:

```typescript
// BUGGY CODE
const previousPartIndex = newParts.findIndex(
  (p) => getToolCallId(p) === toolCallId,
);
const previousPart = newParts.splice(previousPartIndex, 1)[0];
if (!previousPart) {
  newParts.push(part);
  continue;
}
```

When `findIndex` returns `-1` (tool call ID not found in previous parts):
- `splice(-1, 1)` removes the **last element** of the array (JavaScript quirk!)
- `previousPart` is that removed element (truthy, not undefined)
- The new part incorrectly merges with the last element instead of being added
- This corrupts the parts array, causing tool calls to disappear

## Solution

Check for `-1` explicitly before calling splice:

```typescript
// FIXED CODE
if (previousPartIndex === -1) {
  // Tool call not found, add as new
  newParts.push(part);
  continue;
}
const previousPart = newParts.splice(previousPartIndex, 1)[0];
newParts.push(mergeParts(previousPart, part));
```

Fixes get-convex#182

## Test plan
- [x] Test: preserves all tool calls when combining messages
- [x] Test: accumulates tool calls progressively (A → A,B → A,B,C)
- [x] Test: merges tool calls with same toolCallId correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Fixed an issue in message combination that could unintentionally remove elements when specific tool calls were not found.

* **Tests**
  * Added comprehensive test suite validating preservation and progressive accumulation of tool calls, state transitions, and deduplication during message combination.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Update Tailwind configuration to use CommonJS format
…ssage-188

Add userId field to UIMessage type
- Upgrade @convex-dev/rag from 0.6.1 to 0.7.0
  RAG 0.7.0 uses ai@^6.0.0 (compatible with v6), while 0.6.x used
  AI SDK v5 types (EmbeddingModelV2) causing type conflicts

- Fix unused type params in Output interface (src/client/types.ts)
  Prefix with _ to indicate intentionally unused for type signature
  compatibility

- Remove unused vContent import (src/mapping.ts)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add MIGRATION.md with step-by-step upgrade instructions
- Add CLAUDE.md for AI agent guidance
- Add runtime deprecation warnings that fire once per session:
  - textEmbeddingModel → embeddingModel
  - createTool args → inputSchema
  - createTool handler → execute
- Warnings link to migration guide on GitHub

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix race condition in useThreadMessages.ts using generation counter
- Replace {} as any with properly typed token detail objects
- Convert bare URL to markdown link in CLAUDE.md
- Add warning comment about textEmbeddingModel potentially being undefined
- Document why exhaustiveness check is disabled in deltas.ts (forwards compat)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixes the issue where continuation text after tool approval/denial was
appearing in two places - both merged with the original message and as a
separate streaming message.

Key fixes:
1. Add overrideOrder parameter to addMessages to force a specific order
   for continuation messages when forceNewOrder is true
2. Update start.ts to pass overrideOrder when forceNewOrder is set
3. Add error handling in streamText.ts to abort streaming on error
4. Update usage handler to accept AI SDK v6 token detail fields
5. Add tool approval example with approval.ts agent and UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These methods encapsulate the complexity of the AI SDK v6 tool approval
workflow, reducing boilerplate in application code.

Before: ~340 lines of manual tool finding, execution, and message handling
After: ~140 lines using the clean helper API

The helpers handle:
- Finding the tool call info from the approval ID
- Executing the tool (for approval) with proper context injection
- Saving tool-approval-response and tool-result messages
- Continuing generation with forceNewOrder for clean message separation

This is a workaround for AI SDK v6 issue #10980 where the native approval
flow doesn't generate proper tool_result blocks for Anthropic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When pagination cuts off a tool-result from its tool-call, we handle it
gracefully by creating a standalone tool part. The warning was noisy for
this expected pagination behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- inputTokenDetails: noCacheTokens, cacheReadTokens, cacheWriteTokens
- outputTokenDetails: textTokens, reasoningTokens
- raw: kept as v.any() since it's provider-specific

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add tool approval flow to Key Patterns architecture section
- Replace upgrade section with detailed AI-friendly guidance including:
  - Detection patterns for v5 code (parameters, handler, textEmbeddingModel)
  - Dependency update commands
  - Before/after transformation examples for tools, embeddings, step limits
  - Verification steps and common issues
  - New v6 features (tool approval, reasoning streaming, token details)
- Remove outdated TODO comment in deltas.ts (partial tool calls now handled)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enhance CLAUDE.md with comprehensive AI upgrade guidance
When users upgrade to v0.6.0 with old AI SDK v5 dependencies, TypeScript
now shows helpful error messages pointing to the migration guide:

- languageModel: Shows error if model has specificationVersion "v2" instead of "v3"
- createTool args: Shows "⚠️ 'args' was removed... Rename to 'inputSchema'"
- createTool handler: Shows "⚠️ 'handler' was removed... Rename to 'execute'"

This helps users (and AI assistants) understand what needs to change
before they try to run the code.

Also adds scripts/check-upgrade.js CLI tool for scanning codebases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add idempotency guard to prevent duplicate tool execution on retry
  (_findToolCallInfo now returns null if approval already handled)
- Propagate thread userId to tool context for proper auth/scoping
- Remove unnecessary JSON.stringify that corrupted tool outputs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MIGRATION.md to package.json files array so it ships with npm package
- Update compile-time error messages to point to local MIGRATION.md
- Enhance CLAUDE.md with:
  - AI SDK v6 requirement note upfront
  - Compatible sibling packages (@convex-dev/rag@^0.7.0)
  - Type import changes (LanguageModelV2 → V3, EmbeddingModel → EmbeddingModelV3)
  - generateObject mode: "json" removal documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update createTool calls in examples: args -> inputSchema, handler -> execute
- Fix lint errors in check-upgrade.js (Node.js globals, unused catch var)
- Remove unused ToolApprovalRequest/Response imports from mapping.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests cover:
- _findToolCallInfo: finding tool calls, idempotency guard, missing approvals
- UIMessage approval states: pending, approved, denied
- Message grouping with mixed approval/non-approval tools
- Tool result handling when tool call is on previous page
- execution-denied output conversion for provider compatibility
- createTool with needsApproval function and boolean

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests that document actual bugs and edge cases:

BUGS FOUND:
1. Pagination limit: Approvals older than 20 messages won't be found
   (_findToolCallInfo uses hardcoded numItems: 20)

2. Duplicate toolCallId: With duplicate IDs (shouldn't happen but possible),
   code finds first match in iteration order, not the one with the approval

3. Cross-agent tool lookup: approveToolCall fails if called on an agent
   that doesn't have the tool registered, even if approval is valid

4. Tool errors swallowed: Execution errors become string results instead
   of failing the approval workflow properly

5. Race condition (TOCTOU): Concurrent approvals could both pass the
   idempotency check due to non-atomic check-then-act

EDGE CASES HANDLED CORRECTLY:
- Tool call and approval request in different messages
- Missing approvalId in request
- Undefined 'approved' field in response
- String content (non-array) messages
- Tool input with only 'args' (no 'input')
- Tool input with neither 'args' nor 'input'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The approval workflow had a race condition where concurrent approvals
could both pass the idempotency check (in a query) and then both save
duplicate approval responses (in separate mutations).

Fix: Add approvalIdempotencyKey parameter to addMessages mutation.
When provided, the mutation atomically checks if an approval response
with that ID already exists before saving. If it does, returns the
existing message instead of creating a duplicate.

Changes:
- Add approvalIdempotencyKey to SaveMessagesArgs and addMessages mutation
- Update _findToolCallInfo to return { alreadyHandled, existingMessageId }
  instead of null for already-processed approvals
- Update approveToolCall/denyToolCall to:
  1. Handle alreadyHandled case by continuing from existing message
  2. Pass approvalIdempotencyKey to saveMessage for atomic check
- Update tests to reflect new idempotent behavior

The check-and-write is now atomic within a single Convex mutation,
eliminating the race condition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add third-party provider compatibility info to MIGRATION.md
- Add OpenRouter provider upgrade instructions (v1.x → v2.x)
- Add guidance for handling peer dependency conflicts
- Add compatibility table for AI SDK v5 vs v6 providers
- Enhance check-upgrade.js to detect outdated package versions
- Script now checks package.json for incompatible provider versions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the complex approvalIdempotencyKey machinery. Instead, rely on
_findToolCallInfo detecting already-handled approvals and throw an error.
Convex's atomicity guarantees within the mutation are sufficient.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Included AI SDK function `createToolModelOutput` in `shared.ts` to handle the generation of tool model outputs for approval responses, including error handling based on the specified error mode.
- Updated `approveToolCall` in `index.ts` to utilize the new helper function for creating structured tool results.
- Removed the NeverOptional utility type and directly defined the properties for ToolOutputPropertiesCtx.
- Updated the comments to clarify the removal of deprecated parameters `args` and `handler`, emphasizing the use of `inputSchema` and `execute` instead.
- This change simplifies the type structure and avoids potential type issues extending `createTool`.
Copilot AI review requested due to automatic review settings January 28, 2026 10:21
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/zboyles/agent/@convex-dev/agent@10

commit: a6232c5

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements AI SDK v6 tool approval workflow support for @convex-dev/agent, along with dependency updates and various improvements. The PR upgrades the package to require AI SDK v6 (ai@^6.0.0) and adds comprehensive support for tools that need user approval before execution.

Changes:

  • Adds tool approval workflow with Agent.approveToolCall() and Agent.denyToolCall() methods
  • Implements forceNewOrder mechanism to create separate messages for approval continuations
  • Upgrades dependencies to AI SDK v6 (ai@^6.0.35, @ai-sdk/provider-utils@^4.0.6, provider packages v3.x)
  • Adds deprecation warnings for v5 API patterns (argsinputSchema, handlerexecute, textEmbeddingModelembeddingModel)
  • Improves error handling in streaming with try-catch wrapper
  • Refactors React hook to use generation ref pattern instead of isMounted flag
  • Adds comprehensive test coverage for approval workflows including edge cases
  • Adds ChatApproval example demonstrating the approval UI
  • Adds MIGRATION.md and CLAUDE.md documentation files

Reviewed changes

Copilot reviewed 39 out of 42 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/client/index.ts Adds approveToolCall/denyToolCall methods with _findToolCallInfo helper, AI SDK v6 type checks, deprecation warnings
src/client/start.ts Implements forceNewOrder flag to increment message order for approval continuations
src/client/streamText.ts Adds try-catch wrapper to handle streaming errors and abort streams properly
src/client/createTool.ts Updates type definitions to show deprecation errors for args/handler, adds runtime warnings
src/client/types.ts Adds AssertAISDKv6 type check, unused generic prefix convention, forceNewOrder option
src/shared.ts Adds createToolModelOutput helper for tool approval responses
src/mapping.ts Filters out approval parts for provider compatibility, converts execution-denied to text format, improves token detail fields
src/UIMessages.ts Pre-extracts approval parts before toModelMessage filtering, manages approval state with finalStates protection
src/react/useThreadMessages.ts Replaces isMounted flag with generationRef for race condition handling
src/component/messages.ts Adds overrideOrder parameter for forceNewOrder support
example/convex/chat/approval.ts New file demonstrating tool approval workflow
example/ui/chat/ChatApproval.tsx New React UI for tool approval with approve/deny buttons
MIGRATION.md New comprehensive migration guide from v0.3.x to v0.6.0
CLAUDE.md New AI assistant guidance document
docs/* Typo fixes ("an pass" → "and pass", "the" → "then")
package.json Updates dependencies: @convex-dev/rag@0.7.0, convex@^1.31.6, adds MIGRATION.md to files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1801 to +1804
const messagesResult = await this.listMessages(ctx, {
threadId,
paginationOpts: { numItems: 20, cursor: null },
});
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The _findToolCallInfo method only retrieves 20 messages from the thread and searches within them. This creates a limitation where approval requests older than 20 messages cannot be found. This is documented in the test file approval-bugs.test.ts lines 38-89, but it's a significant operational issue.

Consider either:

  1. Increasing the pagination limit (though this has performance implications)
  2. Fetching additional pages if not found in the first 20
  3. Adding an index on approval IDs for direct lookup
  4. Documenting this limitation prominently in the API docs for approveToolCall and denyToolCall

Copilot uses AI. Check for mistakes.
Comment on lines +1786 to +1868
private async _findToolCallInfo(
ctx: ActionCtx,
threadId: string,
approvalId: string,
): Promise<
| {
toolCallId: string;
toolName: string;
toolInput: Record<string, unknown>;
parentMessageId: string;
alreadyHandled?: false;
}
| { alreadyHandled: true; wasApproved: boolean }
| null
> {
const messagesResult = await this.listMessages(ctx, {
threadId,
paginationOpts: { numItems: 20, cursor: null },
});

let toolCallId: string | undefined;
let parentMessageId: string | undefined;
let toolName: string | undefined;
let toolInput: Record<string, unknown> | undefined;

// First, check if this approval has already been handled
for (const msg of messagesResult.page) {
if (msg.message?.role === "tool" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-approval-response" &&
(part as any).approvalId === approvalId
) {
return { alreadyHandled: true, wasApproved: (part as any).approved === true };
}
}
}
}

// Second pass: find the approval request to get toolCallId and parent message
for (const msg of messagesResult.page) {
if (msg.message?.role === "assistant" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-approval-request" &&
(part as any).approvalId === approvalId
) {
parentMessageId = msg._id;
toolCallId = (part as any).toolCallId;
break;
}
}
}
if (toolCallId) break;
}

if (!toolCallId || !parentMessageId) {
return null;
}

// Third pass: find the tool-call with matching toolCallId to get toolName and input
for (const msg of messagesResult.page) {
if (msg.message?.role === "assistant" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-call" &&
(part as any).toolCallId === toolCallId
) {
toolName = (part as any).toolName;
toolInput = (part as any).input ?? (part as any).args ?? {};
break;
}
}
}
if (toolName) break;
}

if (!toolName || !toolInput) {
return null;
}

return { toolCallId, toolName, toolInput, parentMessageId };
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

There is a race condition in the approval workflow as documented in approval-bugs.test.ts lines 581-607. The approveToolCall and denyToolCall methods use separate transactions: one to check if an approval exists via _findToolCallInfo (which calls listMessages), and another to save the approval response via saveMessage. This creates a Time-Of-Check to Time-Of-Use (TOCTOU) vulnerability where two concurrent approval calls could both pass the check and both save responses.

This should be fixed by either:

  1. Moving the check-and-write into a single mutation
  2. Using optimistic concurrency control (e.g., a unique index on approvalId)
  3. At minimum, documenting this limitation prominently

Copilot uses AI. Check for mistakes.
Comment on lines +1811 to +1868
// First, check if this approval has already been handled
for (const msg of messagesResult.page) {
if (msg.message?.role === "tool" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-approval-response" &&
(part as any).approvalId === approvalId
) {
return { alreadyHandled: true, wasApproved: (part as any).approved === true };
}
}
}
}

// Second pass: find the approval request to get toolCallId and parent message
for (const msg of messagesResult.page) {
if (msg.message?.role === "assistant" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-approval-request" &&
(part as any).approvalId === approvalId
) {
parentMessageId = msg._id;
toolCallId = (part as any).toolCallId;
break;
}
}
}
if (toolCallId) break;
}

if (!toolCallId || !parentMessageId) {
return null;
}

// Third pass: find the tool-call with matching toolCallId to get toolName and input
for (const msg of messagesResult.page) {
if (msg.message?.role === "assistant" && Array.isArray(msg.message.content)) {
for (const part of msg.message.content) {
if (
part.type === "tool-call" &&
(part as any).toolCallId === toolCallId
) {
toolName = (part as any).toolName;
toolInput = (part as any).input ?? (part as any).args ?? {};
break;
}
}
}
if (toolName) break;
}

if (!toolName || !toolInput) {
return null;
}

return { toolCallId, toolName, toolInput, parentMessageId };
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The _findToolCallInfo method performs three sequential passes through the message list, which is inefficient. All three pieces of information (approval response, approval request, and tool call) could be extracted in a single pass.

Consider refactoring to:

for (const msg of messagesResult.page) {
  if (!Array.isArray(msg.message?.content)) continue;
  
  for (const part of msg.message.content) {
    // Check all three types in one pass
    if (part.type === "tool-approval-response" && ...) { ... }
    if (part.type === "tool-approval-request" && ...) { ... }
    if (part.type === "tool-call" && ...) { ... }
  }
}

Copilot uses AI. Check for mistakes.
Comment on lines +1832 to +1840
) {
parentMessageId = msg._id;
toolCallId = (part as any).toolCallId;
break;
}
}
}
if (toolCallId) break;
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

When a tool-call and tool-approval-request are in different messages (as tested in approval-bugs.test.ts lines 142-195), the parentMessageId will point to the approval request message rather than the tool-call message. This could cause the approval response to be saved with an incorrect parent relationship, potentially breaking message threading or order logic.

Consider validating that the approval request and tool-call are in the same message, or explicitly handling this edge case.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +171
if (useForceNewOrder) {
// TypeScript can't infer order is defined here from the compound condition above
order = order! + 1;
stepOrder = 0;
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The comment says "TypeScript can't infer order is defined here from the compound condition above" but the non-null assertion order! on line 169 could still fail at runtime if order is undefined. While useForceNewOrder checks order !== undefined, TypeScript correctly doesn't narrow the type because order could be reassigned between lines 164-166.

Consider using a type assertion or restructuring the logic:

if (useForceNewOrder) {
  if (order === undefined) {
    throw new Error("Cannot use forceNewOrder without an existing order");
  }
  order = order + 1;
  stepOrder = 0;
}

Copilot uses AI. Check for mistakes.

const schema = defineSchema({});
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
const action = actionGeneric as ActionBuilder<DataModel, "public">;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Unused variable action.

Suggested change
const action = actionGeneric as ActionBuilder<DataModel, "public">;

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +108
// Agent for testing tool execution without approval
const noApprovalAgent = new Agent(components.agent, {
name: "no-approval-agent",
instructions: "Test agent without approval requirement",
tools: {
checkBalance: checkBalanceTool,
},
languageModel: mockModel({
contentSteps: [
[
{
type: "tool-call",
toolCallId: "call-456",
toolName: "checkBalance",
input: JSON.stringify({ accountId: "ABC123" }),
},
],
[{ type: "text", text: "Your balance is $500." }],
],
}),
stopWhen: stepCountIs(5),
});

Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Unused variable noApprovalAgent.

Suggested change
// Agent for testing tool execution without approval
const noApprovalAgent = new Agent(components.agent, {
name: "no-approval-agent",
instructions: "Test agent without approval requirement",
tools: {
checkBalance: checkBalanceTool,
},
languageModel: mockModel({
contentSteps: [
[
{
type: "tool-call",
toolCallId: "call-456",
toolName: "checkBalance",
input: JSON.stringify({ accountId: "ABC123" }),
},
],
[{ type: "text", text: "Your balance is $500." }],
],
}),
stopWhen: stepCountIs(5),
});

Copilot uses AI. Check for mistakes.
});

expect(firstMsg?.order).toBeDefined();
const initialOrder = firstMsg!.order;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Unused variable initialOrder.

Suggested change
const initialOrder = firstMsg!.order;

Copilot uses AI. Check for mistakes.
) {
await stream;
await result.consumeStream();
streamConsumed = true;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The value assigned to streamConsumed here is unused.

Copilot uses AI. Check for mistakes.
} catch (error) {
// If an error occurs during streaming (e.g., in onStepFinish callbacks),
// make sure to abort the streaming message so it doesn't get stuck
if (streamer && !streamConsumed) {
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

This negation always evaluates to true.

Copilot uses AI. Check for mistakes.
…e raw output directly to `createToolModelOutput`, which handles the conversion properly.
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.

3 participants