cloneThread forwards unsupported options, overcounts remaining limit and retains source parent IDs
- Dominant language
- TypeScript
- Stars
- 349
- Forks
- 92
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 24
Description
Published `@convex-dev/agent@0.7.1` has three independently verified clone defects. The public action fails before copying when either `batchSize` or `limit` is supplied. Behind that first failure, remaining-limit arithmetic can over-copy, and direct native batches retain source-thread `parentMessageId` values.
## 1. Public option forwarding reproduction
Use a fresh directory with Node 22 and these exact dependencies (no app code, deployment or provider credentials):
```bash
npm init -y
npm install --save-exact @convex-dev/agent@0.7.1 convex@1.45.0 convex-test@0.0.56 convex-helpers@0.1.109 ai@7.0.93 @ai-sdk/provider@4.0.10 @ai-sdk/provider-utils@5.0.36 zod@3.25.76 react@19.2.3 vitest@3.2.4
npx vitest run --config vitest.config.mjs
```
`vitest.config.mjs`:
```js
export default { test: { environment: 'node', include: ['repro.test.ts'], fileParallelism: false, maxWorkers: 1, testTimeout: 30000 } };
```
`repro.test.ts`:
```ts
import { afterEach, expect, it, vi } from 'vitest';
import { convexTest } from 'convex-test';
import componentTest from '@convex-dev/agent/test';
import { componentsGeneric, defineSchema } from 'convex/server';
import { createThread, saveMessage, type AgentComponent } from '@convex-dev/agent';
const component = componentsGeneric().agent as unknown as AgentComponent;
function setup() {
const t = convexTest({
schema: defineSchema({}),
modules: { './_generated/server.ts': async () => ({}) },
transactionLimits: true,
});
componentTest.register(t);
return t;
}
afterEach(() => vi.useRealTimers());
it.each([{ batchSize: 2 }, { limit: 2 }])('reproduces unsupported clone option %j', async (option) => {
const t = setup();
const sourceThreadId = await t.mutation(ctx => createThread(ctx, component));
const targetThreadId = await t.mutation(ctx => createThread(ctx, component));
await t.mutation(ctx => saveMessage(ctx, component, { threadId: sourceThreadId, prompt: 'source' }));
await expect(t.action(component.messages.cloneThread, { sourceThreadId, targetThreadId, ...option })).rejects.toThrow('Unexpected field');
});```
On pristine 0.7.1 both cases reject with `Unexpected field`: `cloneThread` spreads the outer-only options into `cloneMessageBatch`, whose validator does not accept them. These assertions document the failure; expected product behavior is a successful bounded clone.
## 2. Remaining limit arithmetic
After isolating the action handler and stubbing only the batch subcall to get beyond the validation failure, `batchSize: 2, limit: 3` requests `[2,2]` and reports 4 copied. The expression uses the full limit again instead of subtracting `copiedSoFar`. The requested page size should derive from `(limit ?? Infinity) - copiedSoFar`, yielding `[2,1]`. This is an isolated handler probe, not a claim that the broken public options currently execute end to end.
## 3. Parent IDs in native batch copies
A real `cloneMessageBatch` call copying one user prompt and its assistant answer to another thread creates a new destination prompt ID, but the destination assistant's `parentMessageId` is still the source prompt ID. Reproduce with two source rows at order 0 / stepOrders 0 and 1, the assistant pointing to the source user row; call the batch with `numItems: 2` and read the two destination rows. The new user row has a different ID; the assistant retains the old ID.
## Suggested fix
Omit both action-only options before each batch call; subtract already-copied rows from the total limit; and remap eligible parent dependencies to destination IDs across batches. Add exact cutoff and replay tests before treating a native clone as an application's branch primitive. Missing endpoints currently widen selection and endpoint filtering is order-based, so applications also need explicit cutoff semantics.
Our application avoids native clone and uses authorized explicit copies. No local clone patch is being proposed as part of this report; these regressions are recorded so a future native implementation can be adopted deliberately.
Contributor guide
Research direction
Start by running the supplied repro.test.ts with the shown Vitest configuration against the listed dependencies. Inspect the cloneThread action and cloneMessageBatch subcall, then add tests for option forwarding, exact limit cutoffs, and parent ID remapping across batches. Done means bounded clones succeed without Unexpected field errors, copy no more than the requested limit, and destination parents reference destination IDs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100