Skip to content

fix: QQBot stream dedup and repair session schema handling#1502

Merged
zerob13 merged 3 commits intodevfrom
fix/qqbot
Apr 21, 2026
Merged

fix: QQBot stream dedup and repair session schema handling#1502
zerob13 merged 3 commits intodevfrom
fix/qqbot

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Apr 21, 2026

Summary

  • Updated QQBot runtime to buffer tool/process output and keep answer delivery final-only, which prevents repeated resend of cumulative snapshots.
  • Added schema repair support for deepchat_sessions.timeout_ms so data migration and recovery paths stay aligned with the actual table definition.
  • Kept the scope tight to QQBot runtime and SQLite session schema, with tests covering both delivery behavior and schema validation.
  • Implementation approach: treat QQBot stream snapshots as cumulative input, derive a lightweight in-memory buffer for tool segments, flush buffered tool messages before terminal output, and keep schema repair logic explicit so runtime recovery does not depend on stale columns.

Testing

  • Added and updated unit tests for QQBot delivery buffering, final-only answer handling, pending/timeout/error paths, and legacy snapshot fallback.
  • Added SQLite schema tests for session table repair and catalog consistency.
  • Validation passed locally with format, i18n, lint, and targeted QQBot runtime tests.

Summary by CodeRabbit

  • New Features

    • Per-session timeout support added to session storage and migrations.
  • Bug Fixes

    • More reliable QQ bot message delivery with improved timeout and final-reply handling.
    • Buffered tool outputs are flushed in correct order before timeouts, pending prompts, or final replies.
  • Chores

    • Database schema and migration logic updated to add timeout column.
  • Tests

    • Expanded tests covering delivery ordering, timeout behaviors, and schema repair.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Refactors QQ Bot delivery state from persisted segments to in-memory per-endpoint tool buffers, adds timeout_ms column support and recovery to deepchat_sessions schema, and expands unit tests for QQBot delivery and SQLite schema repair behavior.

Changes

Cohort / File(s) Summary
QQ Bot Delivery State Refactor
src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts
Replaced persisted delivery-segment sync with an in-memory endpointToolBuffers (per-endpoint QQBotToolBufferState). Updated deliverConversation flow: compute sourceMessageId, syncToolBuffer, flush ready process batches respecting QQBOT_MAX_PASSIVE_REPLIES, and handle terminal/timeout/clear paths. Added QQBOT_TIMEOUT_REPLY constant usage and cleared buffers on stop/error/complete.
QQ Bot Runtime Tests
test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts
Rewrote tests with new helpers and parameterized scenarios (c2c/group). Added timing-driven assertions, flush/ordering rules, final-slot reservation cases, fallback when segments missing, and checks that persisted delivery-state APIs are not called in buffered-flush flows.
SQLite Schema Catalog
src/main/presenter/sqlitePresenter/schemaCatalog.ts
Added timeout_ms to repairableColumns so schema diagnosis/repair recognizes the new column.
DeepChat Sessions Table Migration
src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts
Added conditional ALTER TABLE deepchat_sessions ADD COLUMN timeout_ms INTEGER; to recovery/migration statements when column is missing.
SQLite Presenter Tests
test/main/presenter/sqlitePresenter.test.ts, test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts
New test validating repair of missing timeout_ms in legacy DB; updated migration SQL expectations to include timeout_ms addition for v23/v24 paths.

Sequence Diagram

sequenceDiagram
    participant Client as Inbound Message
    participant Runtime as QQBotRuntime
    participant Buffer as QQBotToolBufferState
    participant Delivery as Delivery System
    participant Remote as Remote Endpoint

    Client->>Runtime: processInboundMessage / deliverConversation
    activate Runtime

    Note over Runtime: derive sourceMessageId & poll snapshots
    Runtime->>Buffer: syncToolBuffer(sourceMessageId, deliverySegments)
    Runtime->>Buffer: identify ready pendingProcessSegments

    loop flush while batches ready
        Runtime->>Buffer: collect pending process texts
        Runtime->>Delivery: enforce QQBOT_MAX_PASSIVE_REPLIES
        Delivery->>Remote: send combined tool/process message
        Remote-->>Delivery: ack
        Runtime->>Buffer: mark flushedProcessKeys
    end

    alt Conversation completes
        Runtime->>Delivery: send final answer text
    else Timeout/Error
        Runtime->>Delivery: flush remaining tool text
        Runtime->>Delivery: send QQBOT_TIMEOUT_REPLY / error reply
    end

    Runtime->>Buffer: clear endpointToolBuffers
    deactivate Runtime
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped through buffers, soft and bright,
Flushed process batches into the night.
Timeout column tucked in rows so deep,
I clear and send — then settle to sleep. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: QQBot stream dedup and repair session schema handling' directly summarizes the main changes: QQBot stream deduplication via buffering and session schema repair for timeout_ms column.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qqbot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zerob13 zerob13 changed the title Fix QQBot stream dedup and repair session schema handling fix: QQBot stream dedup and repair session schema handling Apr 21, 2026
@zerob13 zerob13 marked this pull request as ready for review April 21, 2026 04:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
test/main/presenter/sqlitePresenter.test.ts (1)

721-797: LGTM — solid coverage for the catalog-driven timeout_ms repair path.

Test correctly bootstraps a v24 DB with timeout_ms missing, verifies diagnoseSchema() reports the issue, repairSchema() status is repaired, and post-repair the column exists while the pre-existing reasoning_visibility value is preserved.

Minor nit: the seed value reasoning_visibility = 'auto' is only round-tripped as a raw TEXT here, but if 'auto' is not a member of ReasoningVisibility, using a canonical value (e.g., one accepted by isReasoningVisibility) would make the fixture more faithful to production data. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/sqlitePresenter.test.ts` around lines 721 - 797, The test
seeds reasoning_visibility with the literal 'auto' which may not match the
canonical values; update the fixture in the deepchat_sessions INSERT so the
seeded value is a valid production value accepted by isReasoningVisibility
(e.g., use a member from the ReasoningVisibility enum or the canonical string
the isReasoningVisibility helper expects), then rerun the assertions around
presenter.diagnoseSchema() and presenter.repairSchema() to ensure the value is
round-tripped unchanged.
src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts (1)

164-214: Remove redundant timeout_ms column addition from v23 recovery migration (cleanup only).

The migration system explicitly tolerates ALTER TABLE ADD COLUMN errors when the column already exists (see shouldIgnoreMigrationStatementError in src/main/presenter/sqlitePresenter/index.ts), so v24's unconditional timeout_ms addition will not cause a migration failure even if v23 recovery adds it first. However, the redundant attempt is poor code design.

Either remove timeout_ms from getRecoveryMigrationStatements() (Option A) or make v24 conditional (Option B) to avoid unnecessary error logging and clarify intent. If Option A, also update the test expectation at test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts line 111–117.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts` around lines
164 - 214, The v24 migration unconditionally adds timeout_ms but
getRecoveryMigrationStatements (used by getMigrationSQL(version: 23)) already
includes timeout_ms, causing a redundant ALTER TABLE; remove the duplicate by
either (A) deleting the timeout_ms statement from getRecoveryMigrationStatements
so v23 recovery no longer tries to add it, or (B) make the v24 branch in
getMigrationSQL conditional (check this.hasColumn('timeout_ms') or call
getRecoveryMigrationStatements to avoid adding it) and update the test
expectation in test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts
(lines ~111–117) if you choose option A. Ensure references:
getRecoveryMigrationStatements, getMigrationSQL (version 23 and 24), and
timeout_ms are adjusted accordingly.
test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts (1)

1-1: Mirror the QQBot source subdirectory in the test path.

This suite targets src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts, but the test sits directly under remoteControlPresenter/. Move it to test/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.test.ts so the test tree mirrors the source tree. As per coding guidelines, “Vitest test suites should mirror the source structure under test/main/** and test/renderer/**.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts` at line 1,
Move the test file so the test tree mirrors the source: relocate the current
qqbotRuntime.test.ts from test/main/presenter/remoteControlPresenter/ to
test/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.test.ts so it sits
alongside the source module qqbotRuntime.ts; update any internal import paths in
the test (references to qqbotRuntime, imports at top of the file) to reflect the
new relative path and ensure vitest imports (afterEach, describe, expect, it,
vi) still resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts`:
- Around line 333-336: The code recomputes sourceMessageId each poll
(getConversationSourceMessageId) and getOrCreateToolBuffer currently clears the
buffer when that id changes, causing flushedProcessKeys to reset and resends
when a conversation's source id evolves (e.g., execution.eventId ->
snapshot.messageId); to fix, change getOrCreateToolBuffer/syncToolBuffer logic
to preserve the original buffer identity once any process batches are observed:
detect when flushedProcessKeys (or any processed segments) exist for a
conversation and thereafter treat new sourceMessageId values as aliases
(migrate/merge them into the existing buffer) instead of deleting/creating a new
buffer, or implement a lock that pins the buffer id for that conversation until
the conversation completes; update getOrCreateToolBuffer to check for an
existing pinned buffer key and perform alias migration/merge of deliverySegments
rather than clearing state when sourceMessageId differs.
- Around line 354-360: The code currently always sends finalText from
getFinalDeliveryText() even when it equals the no-response sentinel ("No
assistant response was produced."), causing a noisy extra message after process
output; update the branch around flushBufferedProcessMessages(endpointKey,
sendContext, { reserveTerminalSlot: Boolean(finalText) }) and
sendText(sendContext, finalText) to skip sending when finalText matches the
shared no-response sentinel and process output was actually flushed. Implement
this by checking the same sentinel constant (or string) used elsewhere and a
flag indicating process output was flushed (e.g., have
flushBufferedProcessMessages return a boolean or inspect sendContext/return
value) and only call sendText(sendContext, finalText) when either finalText is
not the sentinel or no process output was flushed; update the related test to
expect the sentinel to be omitted when process output exists.

---

Nitpick comments:
In `@src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts`:
- Around line 164-214: The v24 migration unconditionally adds timeout_ms but
getRecoveryMigrationStatements (used by getMigrationSQL(version: 23)) already
includes timeout_ms, causing a redundant ALTER TABLE; remove the duplicate by
either (A) deleting the timeout_ms statement from getRecoveryMigrationStatements
so v23 recovery no longer tries to add it, or (B) make the v24 branch in
getMigrationSQL conditional (check this.hasColumn('timeout_ms') or call
getRecoveryMigrationStatements to avoid adding it) and update the test
expectation in test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts
(lines ~111–117) if you choose option A. Ensure references:
getRecoveryMigrationStatements, getMigrationSQL (version 23 and 24), and
timeout_ms are adjusted accordingly.

In `@test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts`:
- Line 1: Move the test file so the test tree mirrors the source: relocate the
current qqbotRuntime.test.ts from test/main/presenter/remoteControlPresenter/ to
test/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.test.ts so it sits
alongside the source module qqbotRuntime.ts; update any internal import paths in
the test (references to qqbotRuntime, imports at top of the file) to reflect the
new relative path and ensure vitest imports (afterEach, describe, expect, it,
vi) still resolve.

In `@test/main/presenter/sqlitePresenter.test.ts`:
- Around line 721-797: The test seeds reasoning_visibility with the literal
'auto' which may not match the canonical values; update the fixture in the
deepchat_sessions INSERT so the seeded value is a valid production value
accepted by isReasoningVisibility (e.g., use a member from the
ReasoningVisibility enum or the canonical string the isReasoningVisibility
helper expects), then rerun the assertions around presenter.diagnoseSchema() and
presenter.repairSchema() to ensure the value is round-tripped unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50f509a0-5fb3-4a35-9738-1c617383e025

📥 Commits

Reviewing files that changed from the base of the PR and between fc012db and 159e731.

📒 Files selected for processing (6)
  • src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts
  • src/main/presenter/sqlitePresenter/schemaCatalog.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts
  • test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts
  • test/main/presenter/sqlitePresenter.test.ts
  • test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts

Comment thread src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts
Comment thread src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts (1)

284-289: Optional: extract the repeated tool buffer + delivery state cleanup.

The pair clearToolBuffer(endpointKey) + bindingStore.clearRemoteDeliveryState(endpointKey) recurs in five places (error path here, and in deliverConversation at lines 325-326, 350-351, 368-369, 379-380). A small helper would make it harder to forget one side of the cleanup in future branches.

♻️ Proposed refactor
+  private resetEndpointDeliveryState(endpointKey: string): void {
+    this.clearToolBuffer(endpointKey)
+    this.deps.bindingStore.clearRemoteDeliveryState(endpointKey)
+  }

Then replace the paired calls with this.resetEndpointDeliveryState(endpointKey).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts` around lines
284 - 289, Extract the repeated cleanup pair into a single helper on the class
(e.g., add a method resetEndpointDeliveryState(endpointKey) that calls
this.clearToolBuffer(endpointKey) and
this.deps.bindingStore.clearRemoteDeliveryState(endpointKey)); then replace each
occurrence of the pair (currently calls to clearToolBuffer(endpointKey) +
this.deps.bindingStore.clearRemoteDeliveryState(endpointKey) — seen in the error
path around the flush + sendText block and the various deliverConversation
branches) with a single call to this.resetEndpointDeliveryState(endpointKey) to
ensure both sides are always cleaned together.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts`:
- Around line 284-289: Extract the repeated cleanup pair into a single helper on
the class (e.g., add a method resetEndpointDeliveryState(endpointKey) that calls
this.clearToolBuffer(endpointKey) and
this.deps.bindingStore.clearRemoteDeliveryState(endpointKey)); then replace each
occurrence of the pair (currently calls to clearToolBuffer(endpointKey) +
this.deps.bindingStore.clearRemoteDeliveryState(endpointKey) — seen in the error
path around the flush + sendText block and the various deliverConversation
branches) with a single call to this.resetEndpointDeliveryState(endpointKey) to
ensure both sides are always cleaned together.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 324cdf84-48f5-4ab8-b615-94193197c645

📥 Commits

Reviewing files that changed from the base of the PR and between 159e731 and a3474a5.

📒 Files selected for processing (2)
  • src/main/presenter/remoteControlPresenter/qqbot/qqbotRuntime.ts
  • test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/main/presenter/remoteControlPresenter/qqbotRuntime.test.ts

@zerob13 zerob13 merged commit f58dce7 into dev Apr 21, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the fix/qqbot branch April 24, 2026 08:52
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.

1 participant