cloudflare / cloudflare/agents

@cloudflare/think continuation recovery can append duplicate assistant message

Open
#1,876 0 comments 2 reactions 1 assignee Claimed by @threepointone View on GitHub
think
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

## Summary

`@cloudflare/think` recovery continuation can append a second assistant message instead of continuing the interrupted assistant message.

In `Think._streamResult`, a recovery continuation currently creates a fresh `StreamAccumulator` with a new `crypto.randomUUID()` message id. When the interrupted turn already has a persisted assistant message and recovery continues that same logical assistant turn, the accumulator should reuse that assistant message id/parts/metadata. Otherwise the recovered transcript can contain two assistant messages for one user turn.

This is distinct from #1691. That issue covered a normal new assistant response being merged into a previous assistant message in `@cloudflare/ai-chat`. This issue is the opposite shape in `@cloudflare/think`: a continuation of the same assistant turn can become a duplicate assistant row.

## Observed version

- `@cloudflare/think@0.12.1`
- `agents@0.17.3`
- Also present on current `cloudflare/agents` `main` as of commit `5ee3bf5`.

## Why this matters

This is a recovery correctness issue. A Durable Object reset, deploy churn, isolate restart, or stream recovery path should preserve one logical assistant turn. Appending a fresh assistant message for a continuation changes both the visible transcript and later model context.

Expected transcript shape:

```ts
user
assistant // interrupted partial answer, then continued in-place
```

Actual shape:

```ts
user
assistant // interrupted partial answer
assistant // continuation appended as a second assistant message
```

## Suspected cause

In `packages/think/src/think.ts`, the continuation path activates pending continuation state, but the stream accumulator is initialized with a fresh message id:

```ts
const accumulator = new StreamAccumulator({ messageId: crypto.randomUUID() });
```

For `continuation === true`, it should initialize from the latest assistant message already present in `this.messages`:

```ts
const continuationAssistant = continuation
? [...this.messages].reverse().find((message) => message.role === "assistant")
: undefined;

const accumulator = new StreamAccumulator({
messageId: continuationAssistant?.id ?? crypto.randomUUID(),
continuation,
existingParts: continuationAssistant?.parts,
existingMetadata: continuationAssistant?.metadata
});
```

## Reproduction shape

A regression test that reproduces this is in the patch branch below. It:

1. Persists a user message.
2. Persists an interrupted assistant message with id `a-continuation-accumulator`.
3. Inserts interrupted stream chunks and a recovery fiber snapshot for that assistant.
4. Triggers chat recovery continuation.
5. Asserts there is exactly one assistant message and that it keeps id `a-continuation-accumulator`.

## Patch branch

Because this repo README currently says external PRs are not being accepted, I am filing this issue first and linking the patch branch instead of treating a PR as the durable contribution path.

Compare branch:

https://github.com/cloudflare/agents/compare/main...advaitpaliwal:codex/think-continuation-accumulator

Patch contents:

- `packages/think/src/think.ts` — initialize `StreamAccumulator` from the latest assistant message during continuation.
- `packages/think/src/tests/run-turn-recovery.test.ts` — regression test for duplicate assistant prevention.
- `.changeset/think-continuation-accumulator.md` — patch changeset for `@cloudflare/think`.

## Local validation on patch branch

```sh
pnpm --filter @cloudflare/think exec vitest --run -c src/tests/vitest.config.ts src/tests/run-turn-recovery.test.ts
pnpm exec oxfmt --check packages/think/src/think.ts packages/think/src/tests/run-turn-recovery.test.ts .changeset/think-continuation-accumulator.md
pnpm exec oxlint packages/think/src/think.ts packages/think/src/tests/run-turn-recovery.test.ts
```

All three passed locally.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.