withPersistence does not save activity rows when a run is stopped or fails
Maintainer thường phản hồi trong vòng 1 ngày
@tombeckenham đang làm issue này rồi.
Từ ngày 16/10/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
TanStack AI version
@tanstack/ai 0.69.0, @tanstack/ai-persistence 0.9.0 (the same behavior is on main at 85c2956)
Framework/Library version
Node 26.3.1, server only (no UI framework involved)
Describe the bug and the steps to reproduce it
With an activities store, withPersistence saves AG-UI activity rows when a run finishes, but not when the run is stopped or fails. After a reload, reconstructChat returns no activity row for a stopped or failed run, even though the client showed it live.
Activity rows and the activities store came in #1323 (issue #1286).
In packages/ai-persistence/src/middleware.ts, persistActivities(ctx) runs in onStart, in the throttled text snapshot (only with snapshotStreaming), at an interrupt, and in onFinish. onError and onAbort call updateStreamingRows and failRun / abortRun, but not persistActivities. So a stopped or failed run keeps only the activity state of the last text snapshot. Without snapshotStreaming, or when no text was streamed after the activity, it keeps none of the activity from that run.
This is about the activity store (stores.activities, which withPersistence writes and reconstructChat reads). It is not about the durability log, the delivery stream that toServerSentEventsResponse({ durability }) appends to. The message store (stores.messages) does mark the stopped or failed assistant row through updateStreamingRows. Only the activity rows are left behind.
A progress row is most useful on exactly these runs. For example, a search card that says "searching" or "found 3 sources" is the only trace of the work when the user presses Stop or the provider fails.
Steps:
- Install
@tanstack/[email protected]and@tanstack/[email protected]. - Save the script below as
repro.mjsin an ESM package and runnode repro.mjs.
repro.mjs:
import { chat } from '@tanstack/ai'
import { memoryPersistence, reconstructChat, withPersistence } from '@tanstack/ai-persistence'
async function run(label, end) {
const persistence = memoryPersistence()
const controller = new AbortController()
const threadId = `thread-${label}`
const adapter = {
kind: 'text',
name: 'fixed',
model: 'test-model',
'~types': {},
async *chatStream({ runId }) {
const t = Date.now()
yield { type: 'RUN_STARTED', threadId, runId, timestamp: t }
yield {
type: 'ACTIVITY_SNAPSHOT',
messageId: 'search-1',
activityType: 'search',
content: { status: 'running' },
timestamp: t,
}
if (end === 'stop') controller.abort() // the user presses Stop
if (end === 'error') throw new Error('provider failed')
yield { type: 'RUN_FINISHED', threadId, runId, finishReason: 'stop', timestamp: t }
},
}
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'search' }],
threadId,
runId: `run-${label}`,
abortController: controller,
middleware: [withPersistence(persistence)],
})
try {
for await (const _ of stream) void _
} catch {}
const stored = await persistence.stores.activities.loadActivities(threadId)
const res = await reconstructChat(persistence, new Request(`https://x/chat?threadId=${threadId}`))
const roles = (await res.json()).messages.map((m) => m.role)
console.log(`${label}: stored activities ${stored.length}, reconstructChat roles ${JSON.stringify(roles)}`)
}
await run('finish', 'finish')
await run('stop', 'stop')
await run('error', 'error')
Expected
finish: stored activities 1, reconstructChat roles ["user","activity"]
stop: stored activities 1, reconstructChat roles ["user","activity"]
error: stored activities 1, reconstructChat roles ["user","activity"]
Actual
finish: stored activities 1, reconstructChat roles ["user","activity"]
stop: stored activities 0, reconstructChat roles ["user"]
error: stored activities 0, reconstructChat roles ["user"]
(The error run also logs the expected chat run failed error. It is left out above.)
A likely fix is to call persistActivities(ctx) in onError and in the terminal branch of onAbort, after failRun / abortRun, so the run record is terminal first. A plain disconnect on a detachable or interrupted run would stay as it is now (nothing written).
One limit applies. persistActivities catches a rejected save, so a failing activity store cannot change how the run ends. It still awaits the save, though. A save that hangs would hold up the hook, and with it the completion promise (waitForRunCompletion) if the save runs before that promise is rejected. onFinish has the same exposure today, because it awaits persistActivities before it resolves the completion. If that matters here, the save could run after the completion promise settles, or take a timeout.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
Inline above. Plain Node script, no network, no model.
Screenshots or Videos (Optional)
None.
Do you intend to try to help solve this bug with your own PR?
Maybe, I'll investigate and start debugging
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- Ngôn ngữ chính
- TypeScript
- Star
- 3.2k
- Fork
- 365
- Merge trung bình
- 1 ngày 9 giờ
- Pull request đã merge (30 ngày)
- 242
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/ai
-
Activity events that a chat middleware emits are streamed but never recorded or persistedCó thể đã có người làm @AlemTuzlak đã nhận 4 ngày trước. Đang mởwaiting-on: maintainer
TanStack/ai#1803 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
withPersistence stores the durability batch hint on a stopped or failed assistant messageCó thể đã có người làm @tombeckenham đã nhận 4 ngày trước. Đang mởhas-pr waiting-on: maintainer
TanStack/ai#1802 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Discriminate status-dependent record and option types (RunRecord, SessionIndexEntry, InputState, durable-only options)Có thể đã có người làm @jherr đã nhận 5 ngày trước. Đang mởenhancement waiting-on: maintainer
TanStack/ai#1799 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ai-harness: an input with no principal runs with the owner session's principalCó thể đã có người làm @tombeckenham đã nhận 5 ngày trước. Đang mởwaiting-on: maintainer
TanStack/ai#1797 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ai-harness: a forwarded command completes with no result, also when the owner rejects itCó thể đã có người làm @tombeckenham đã nhận 5 ngày trước. Đang mởwaiting-on: maintainer
TanStack/ai#1796 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
area:editor bug issue-form:bug P0 platform:windows review: low
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
1lck/Lithe-IDEA#1388 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
fix(api): isToolAuditEnabled swallows database read errors on auditToolOperations without loggingĐang mởarea: backend bug priority: low
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
snapotter-hq/SnapOtter#3238 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
rig up --plan times out after 5 s and reports an UNKNOWN outcome while the daemon completes the planĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
filesystem: search_files does not expand non-wildcard excludePatterns like directory_tree doesĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
modelcontextprotocol/servers#5216 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày