Skip to content

Activity-ordered session cursors can duplicate a row when a live entry retires mid-pagination #9419

Description

@doudouOUC

Split out of #9396 review.

Problem

The activity-ordered session list cursors order rows by the later of the persisted transcript mtime and the bridge's in-memory live activity watermark. The watermark is not durable, so the ordering key is not monotonic per session identity: when a session's live entry retires between two page fetches, its key falls back to mtime, and a page whose cursor was encoded from the higher watermark re-admits that row.

Affected surfaces (all use an opaque activity cursor):

  • GET /workspace/:id/sessions?view=organized
  • GET /workspace/:id/sessions with parentSessionId or sourceType

Reproduction shape: persisted session S has mtime M; the bridge watermark for S is W > M; page size 1; S is emitted on page 1 keyed by W; S's live entry retires; page 2's strictly-older-than-cursor filter admits S again because M < W.

Before the watermark existed, activity keys came from mtime alone and could only advance, so pagination could skip a row but never repeat one. Duplication is the new mode.

Why it was not fixed in #9396

No stateless fix preserves the feature:

  • Encoding the cursor from the persisted key while sorting by the merged key deterministically skips every row whose merged key lands between the tail row's persisted and merged keys — strictly worse than a race-window duplicate.
  • Dropping the watermark from the ordering key contradicts the feature: a completed turn would no longer move its session in activity order.
  • Preserving retired watermarks re-introduces durability the protocol deliberately does not claim, and needs an eviction policy.
  • Flushing the recorder before publishing each terminal was rejected in the design: it turns UI recency into a synchronous durability barrier on the prompt response path.

A correct fix needs the cursor to carry the highest key already emitted per identity so the after-cursor filter can compare against that instead of the row's current key. That changes the opaque cursor payload on every activity-cursor route, so it is a pagination-contract change rather than part of a timestamp change.

Current exposure

No first-party consumer accumulates multiple pages from these routes:

  • web-shell requests view=organized but only ever loads one page; it never consumes nextCursor.
  • The CLI session picker does paginate, but on the default numeric-cursor path.
  • The VS Code companion does not paginate session lists.

So the exposure is limited to SDK consumers writing their own pagination loops. #9396 documents the behavior in docs/developers/qwen-serve-protocol.md and the design doc, and tells such callers to key accumulated pages by sessionId.

Scope

  • Extend the organized / metadata activity cursor payloads to carry per-identity emitted keys (bounded by the live-inflated rows observed during the pass).
  • Apply the recorded key in the after-cursor filter instead of the row's current merged key.
  • Regression test: a live entry with W > M retires between two page fetches; the session must appear exactly once across the pass.
  • Decide and document the cursor size bound and what happens when it is exceeded.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    daemonpriority/P3Low - Minor, cosmetic, nice-to-fix issuesscope/session-managementSession state and persistencetype/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions