Skip to content

fix(chat): 修复长回合终态历史落库失败导致对话丢失 - #445

Merged
su-fen merged 4 commits into
Stack-Cairn:mainfrom
thirsty5034:fix/chat-history-final-persist
Aug 13, 2026
Merged

fix(chat): 修复长回合终态历史落库失败导致对话丢失#445
su-fen merged 4 commits into
Stack-Cairn:mainfrom
thirsty5034:fix/chat-history-final-persist

Conversation

@thirsty5034

@thirsty5034 thirsty5034 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

关联

Fixes #440

问题

长 agent 回合结束后,内存里已有完整对话,但 SQLite 可能仍停在开局时的 仅用户消息 快照。
刷新/重开会话后助手回复丢失;与此同时记忆提取仍可能基于内存状态执行,造成“对话丢了、记忆却写了”的不一致。

这是 GUI/WebUI 共用历史链路问题,不是 headless 专属。

上游 issue:#440(用户侧表现为“历史记录保存失败 / 重启后对话丢失”)。

根因

  1. 发送路径会较早落库 user-only 快照。
  2. agent 主要在内存推进;回合结束的 persistConversationRuntime 一旦失败会被当成非致命 soft-fail。
  3. 多段 compaction 时,内存 active segment 可能相对持久化游标跳多段,旧逻辑直接抛 不支持的历史分段跳变,整次终态落库失败。
  4. 失败后仍可能继续 memory extraction。

改动

  • 终态历史写入增加短重试(默认 3 次 / 150ms 退避),保留 soft false 与 throw 的既有契约
  • 多段跳变改为 按段 catch-up append,每成功一段就提交 cursor,后续失败可从持久化前沿续写
  • 写入前对齐 header/segment 的 messageCount 与 live messages.length,并同步中间 append 的 contextMetaJson totals
  • agent / text 回合仅在终态历史落库成功后才跑 memory extraction
  • 补充回归:多段追赶、部分续写、终态重试成功/耗尽

验证

本地(crates/agent-gui):

  • pnpm exec biome check(本次改动文件)通过
  • pnpm exec tsc --noEmit 通过
  • node --test test/chat/chat-history-persist-queue.test.mjs test/chat/chat-stop-timing.test.mjs18/18
  • pnpm lint:全量仅有既有 warn(useWorkspaceProjects 等,非本 PR 引入;CI 此前也只因本 PR format error 失败)

Screenshots / preview

逻辑层持久化修复,无界面布局变更。以下引用 #440 用户复现截图作为 before 场景说明:

issue #440 复现截图

说明

--- images --- ['https://github.com/user-attachments/assets/f2f04143-acf1-490f-9629-666d3227a9a8']

Long agent turns could finish in memory while the durable SQLite snapshot stayed on the initial user-only write. Final persist now retries transient failures, catches up multi-segment compaction jumps one append at a time, aligns header/segment counts before write, and only runs memory extraction after history lands.
@StackCairn
StackCairn marked this pull request as draft August 13, 2026 03:33
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

@thirsty5034 thirsty5034 changed the title fix(chat): reliable final history persist after long agent turns fix(chat): 修复长回合终态历史落库失败导致对话丢失 Aug 13, 2026
@thirsty5034
thirsty5034 marked this pull request as ready for review August 13, 2026 03:48
…ments

The prior fix derived header total_segment_count/total_message_count by
summing the in-memory segments. After a conversation is reopened from
history, state.segments holds only the loaded active segment while sealed
segments live solely in SQLite, so that sum undercounts. Rust's
verify_chat_history_consistency compares the header against COUNT/SUM over
all rows in the same transaction and rejects every persist — reproducing
the exact history loss this PR targets, and now deterministically.

Anchor final-persist header totals on state.meta (maintained incrementally
by appendMessagesToConversation and reconciled by normalizeConversationState,
so it already matches the durable segment sum). For intermediate catch-up
appends, derive each step's totals from the final header minus the
not-yet-appended in-memory segments, and set totalSegmentCount to
activeSegmentIndex + 1 so it equals stored_count + 1 as the append
precondition demands.

Add regression coverage for reopened conversations (active-segment upsert
and multi-segment catch-up) whose meta totals exceed the in-memory segment
sum; fix the multi-jump assertion to expect the per-step segment count.
@su-fen

su-fen commented Aug 13, 2026

Copy link
Copy Markdown
Member

在原改动基础上追加了一个 commit(0a44d8ac),修掉两处会导致每次终态落库被后端确定性拒绝的计数缺陷——它们恰好命中并放大了本 PR 要修的历史丢失场景。已在评审时用差分探针实证,现附上机制与验证。

问题 1 — 终态 header 计数对内存段求和(P0)

buildChatHistoryConversationInput 里的 derivedTotalMessageCount / derivedTotalSegmentCount 是对 state.segments 求和得来的。但从历史重开会话时(openInitialbuildConversationStateFromWindow),state.segments 只含活跃分段,封存段只在 SQLite 里;而 state.meta.totalMessageCount 仍是全会话总数。于是求和会少算所有封存段。

Rust 端 verify_chat_history_consistency(segments.rs)在 upsert / upsert_active_segment / append_segment 三条写路径的事务内,都用 header 总数与 DB 全部 segment 行的 SUM(message_count) 比对,不匹配即拒绝("segment/message 统计不匹配")。这是确定性失败,新增的 3 次重试也救不回来——每回合都弹"历史记录保存失败",正是 #440 症状,且触发条件不苛刻(会话切换后空闲缓存被逐出,重开即走此路径,不需重启)。

差分实证(DB 布局 seg0=100/seg1=80/seg2=90/seg3=22,重开后内存只有 seg3):

分支 发给后端的 header totalMessageCount 后果
main 292 校验通过
本 PR(修复前) 22 校验必拒

问题 2 — catch-up 中间步 totalSegmentCount 超前(P0)

conversationInputForCursor 除了同样对内存段求和,totalSegmentCount 还取 Math.max(现有值, 步号+1)。而 validate_append_segment_preconditions(segments.rs:172)严格要求 totalSegmentCount == 库里现有段数 + 1。也就是说,连"纯内存一回合压缩多次"这个本 PR 主打的修复,在真实后端也会被拒。

修法

  • 终态 header 计数改回锚定 state.meta:它由 appendMessagesToConversation 增量维护、normalizeConversationState 按丢弃数扣减,天然等于 DB 全表 SUM。
  • catch-up 中间步改为精确值:totalSegmentCount = activeSegmentIndex + 1;totalMessageCount = 最终 header 总数 − 尚未追加的内存段消息数。对纯内存和重开两种形态都成立。
  • 原 PR 的有效部分全部保留:终态短重试、逐段追赶 + 游标前沿续写、记忆提取门控到落库成功之后。

为什么 18/18 测试没拦住

测试夹具的 buildState/segment() 只构造"全部分段都在内存"形态,表达不了"meta 计数 ≠ 内存段计数"的重开形态。本 commit 给 buildState 加了 metaOverrides,并补了两个重开回归(active-segment upsert 与多段 catch-up append),它们在修复前的代码上会失败;同时修正了 final persist catches up… 里错误的中间步断言(totalSegmentCount 3→2)。

验证

目标两个测试套件 20/20biome checktsc --noEmit 本地通过;远端 CI 全绿。

一点说明

这仍是逻辑层加固。#440 的原始触发链(中转 API 余额耗尽时的具体失败)未被真正复现——本 PR 能让"重开已压缩会话必败"和"多段压缩 append 必败"两个确定性缺陷消失,是净改进,但建议合入后请报告者在真实场景升级验证后再关闭 issue。

thirsty5034 added a commit to thirsty5034/LiveAgent that referenced this pull request Aug 13, 2026
…otals

Fork main has no taskState/taskList on StoredChatContextMeta; keep Stack-Cairn#445
catch-up/retry helpers and assert intermediate append totals as
activeIndex+1 so tsc and history persist tests pass on headless main.
Agent Dev memory extraction can write durable memory before the final chat snapshot lands, leaving memory ahead of a user-only history when persistence fails. Persist the completed answer first, gate extraction on that success, then persist any render-only extraction status separately.
@su-fen
su-fen merged commit 14844e1 into Stack-Cairn:main Aug 13, 2026
8 checks passed
thirsty5034 pushed a commit to thirsty5034/LiveAgent that referenced this pull request Aug 13, 2026
… and keep headless

Bring upstream main (including merged PR Stack-Cairn#445 chat history final-persist)
into the fork while preserving the headless runtime, tauriBridge adapters,
and desktop/headless command routing. Resolve conflicts by taking upstream
feature code paths and re-applying headless-compatible invoke/listen/openUrl
bridges, AppendSegment input types, and worktree command registration.
thirsty5034 added a commit to thirsty5034/LiveAgent that referenced this pull request Aug 13, 2026
 sync

chat_history_set_cwd / git worktree commands / HistorySetCwd gateway
handler still referenced tauri after the upstream merge, breaking
--no-default-features GHCR builds.
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.

[Bug] 从 1.2.3 升级到 1.2.4 后出现“历史记录保存失败”,重启导致对话丢失

2 participants