Skip to content

Preserve history when deduplicating agent sessions#471

Open
Liu-KM wants to merge 1 commit intotiann:mainfrom
Liu-KM:liu/pr-agent-session-history-merge
Open

Preserve history when deduplicating agent sessions#471
Liu-KM wants to merge 1 commit intotiann:mainfrom
Liu-KM:liu/pr-agent-session-history-merge

Conversation

@Liu-KM
Copy link
Copy Markdown

@Liu-KM Liu-KM commented Apr 16, 2026

Summary

Preserve conversation history when multiple HAPI sessions point at the same underlying agent session ID.

  • merge persisted messages from duplicate agent sessions into the canonical visible session
  • keep active duplicate session records alive so existing sockets/keepalives do not break
  • delete inactive duplicates after their history is merged
  • coalesce dedup triggers so a session that becomes inactive during dedup is handled by a follow-up pass

Why

The sidebar deduplicates sessions by agent session ID, but historical messages could remain attached to the hidden duplicate. That made the visible session look like it had lost history.

Validation

  • cd hub && bun test src/sync/sessionModel.test.ts

Notes

AI-assisted with GPT-5.4.

@Liu-KM Liu-KM marked this pull request as ready for review April 16, 2026 15:48
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Active-duplicate permission requests are now routed to the wrong CLI session. The new active path calls mergeSessionHistory() for a live duplicate (hub/src/sync/sessionCache.ts:683), which still copies oldStored.agentState.requests onto newSessionId; approvals then look up the request on the visible session and dispatch RPCs by that same session id (hub/src/web/routes/permissions.ts:52, hub/src/sync/rpcGateway.ts:54). If the pending request originated on the kept-alive duplicate, the UI will show it on the canonical session but the approve/deny call will go to the wrong socket.
  • [Major] History moved from an active duplicate will not appear in an already-open chat until a manual refresh. This new path moves rows in SQLite but only emits session-updated (hub/src/sync/sessionCache.ts:538), while the web message window is refreshed only on message-received or explicit fetchLatestMessages (web/src/hooks/useSSE.ts:500, web/src/hooks/queries/useMessages.ts:59).

Summary
Review mode: initial

Two regressions found in the new active-duplicate merge path; details are attached inline.

Testing
Not run (automation)

HAPI Bot

// persisted history into the visible dedup target. This preserves
// left-sidebar dedup while making resumed/restarted sessions show
// the full conversation history.
await this.mergeSessionHistory(id, targetId, targetNamespace)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This new branch sends active duplicates through mergeSessionHistory(), which also merges oldStored.agentState into targetId. The problem is that permission approval/deny still routes by session id, so a pending request raised by the kept-alive duplicate will now be shown on the canonical session and then sent to the wrong CLI socket when the user acts on it.

Suggested fix:

if (candidate?.active) {
    await this.mergeSessionHistory(id, targetId, targetNamespace, {
        mergeAgentState: false,
    })
}

this.refreshSession(newSessionId)
const refreshed = this.refreshSession(newSessionId)
if (refreshed) {
this.publisher.emit({ type: 'session-updated', sessionId: newSessionId, data: refreshed })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This history-only merge rewrites message rows in SQLite, but it only publishes session-updated afterwards. The web cache appends on message-received and otherwise reloads messages only on mount/refetch, so an already-open canonical chat keeps showing the pre-merge transcript until the user manually refreshes.

Suggested fix:

const moved = this.store.messages.mergeSessionMessages(oldSessionId, newSessionId)
if (moved.moved > 0) {
    this.publisher.emit({ type: 'messages-invalidated', sessionId: newSessionId })
}

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