`ChatEngine.handleTextMessageContentEvent` is O(n²) in answer length: `startsWith(before)` copies the whole accumulated text on every chunk
I maintainer di solito rispondono entro 1 giorno
@tombeckenham ci sta già lavorando.
Dal 30/10/2026.
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
What happens
handleTextMessageContentEvent in packages/ai/src/activities/chat/index.ts (main @ a7eb584, L2405–L2431) runs this for every TEXT_MESSAGE_CONTENT chunk:
const before = this.accumulatedContent
if (typeof extra.content === 'string' && extra.content !== '') {
this.accumulatedContent = extra.content
} else {
this.accumulatedContent += chunk.delta
}
// ...
if (!this.accumulatedContent.startsWith(before)) {
this.turnParts = null
return
}
const delta = this.accumulatedContent.slice(before.length)
After +=, V8 doesn't copy the text. It stores the result as a string that points to the two parts. startsWith then forces V8 to copy the whole accumulated answer into one string, on every chunk. So the total cost grows with the square of the answer length, and none of it yields to the event loop.
Reproduction
This uses the same string operations, with no TanStack code involved. Node 22, Apple Silicon:
const N = 195_000
const chunk = 'abcd'
let acc = ''
const t0 = performance.now()
for (let i = 0; i < N / chunk.length; i++) {
const before = acc
acc += chunk
if (!acc.startsWith(before)) {
throw new Error('unreachable')
}
acc.slice(before.length)
}
console.log(performance.now() - t0)
- With the
startsWith, it takes about 10.9 s. - With only the
+=, it takes about 0 ms.
In a real chat() run, a CPU profile of a stream of about 195k characters in 4-character chunks showed about 15.7 s inside handleStreamChunk. A model that returns a long answer in small chunks blocks the server's event loop for that whole time.
We noticed it in a test that streams a large fake answer four characters at a time. Writing the same bytes as 3-byte CJK characters cut the character count to a quarter and the runtime by more than ten times, about what O(n²) in string length predicts. A model that streams a long answer in small deltas hits the same cost in production.
Affected versions: @tanstack/ai 1.0.0 and 1.0.1, and current main.
Suggested fix
The prefix check only matters when an adapter sends a cumulative content snapshot. When the chunk is a plain delta, the new text starts with before by definition, and the new text is chunk.delta. So the check can move into the snapshot branch:
const extra = chunk as AdapterYieldChunk
const before = this.accumulatedContent
let delta: string
if (typeof extra.content === 'string' && extra.content !== '') {
this.accumulatedContent = extra.content
if (this.turnParts && !extra.content.startsWith(before)) {
// A cumulative `content` snapshot rewrote earlier text; order unknown.
this.turnParts = null
}
delta = extra.content.slice(before.length)
} else {
this.accumulatedContent += chunk.delta
delta = chunk.delta
}
this.middlewareCtx.accumulatedContent = this.accumulatedContent
if (!this.turnParts) return
if (delta === '') return
const last = this.turnParts[this.turnParts.length - 1]
if (last && last.type === 'text') {
last.content += delta
} else {
this.turnParts.push({
type: 'text',
content: delta,
})
}
With this change, a stream of deltas does constant work per chunk. Snapshot chunks still copy the text, but they are the rare fallback path.
A regression test could stream about 200k characters of 4-character deltas through chat() with a mock adapter, then check that the turn's text part matches the input and that the run takes well under a second. If you'd rather not test timing, assert on the result and treat the benchmark above as the guard.
- Lingua principale
- TypeScript
- Stelle
- 3.2k
- Fork
- 372
- Merge medio
- 1g 5h
- PR unite (30g)
- 233
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di TanStack/ai
-
openai-base: a Responses max_output_tokens cutoff ends as RUN_ERROR without usage instead of finishReason 'length'Forse già presa @tombeckenham l’ha presa 1 giorno fa. Apertahas-pr waiting-on: maintainer
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
TanStack/ai#1916 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 1-2 giorni Idoneità per principianti 58/100
I maintainer di solito rispondono entro 1 giorno
-
openai-base: streamed `apply_patch` calls reach the app with an empty `diff`Forse già presa @tombeckenham l’ha presa oggi. Apertahas-pr waiting-on: maintainer
TanStack/ai#1947 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
needsApproval client tools can't be approved without a server (custom connection adapters)Forse già presa @jherr l’ha presa oggi. Apertawaiting-on: maintainer
TanStack/ai#1945 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
feat: live speech-to-text sessions for voice and telephony agentsForse già presa @AlemTuzlak l’ha presa 1 giorno fa. Apertawaiting-on: maintainer
TanStack/ai#1944 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
bug
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 86/100
awslabs/visual-asset-management-system#937 ·
I maintainer di solito rispondono entro 1 giorno
-
t0ggles
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
nolimits4web/swiper#8255 · 1 commento ·
I maintainer di solito rispondono entro 15 giorni
-
render: --variables-file (and --variables <path>) fails with "io.readFile is not a function" since 0.8.212Forse già presa @miguel-heygen l’ha presa oggi. Apertabug triage/needs-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
heygen-com/hyperframes#6830 ·
I maintainer di solito rispondono entro 1 giorno
-
bug launch local use
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
I maintainer di solito rispondono entro 1 giorno
-
account: profile bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 5 giorni