Skip to content

Design: tree-shaped prefix affinity — forked branches inherit the shared prefix's compression state #629

Description

@ranxianglei

Background

Anonymous prefix affinity (#309/#316/#499, src/prefix-affinity.ts) resolves header-less clients by longest-prefix match over an append-only hash chain (h_i = sha256(h_{i-1} || msg_i)). It deliberately mirrors vLLM/SGLang radix prefix caching at the identity layer.

Current fork semantics (documented in the file header): when a request's history diverges from every stored chain, it forks into a brand-new session with zero compression state. The discarded parent candidates are recorded in lineage (parents[], reason: "forked"|"truncated") — but lineage is UI/debug only, never used for matching or state inheritance (MIN_FORK_PREFIX = 3, comment says so explicitly).

The gap

A fork (user edits history, rewinds, or two parallel conversations share a common opening) throws away every compression block anchored inside the shared prefix:

  • the parent session may have folded 100K tokens of that prefix into blocks;
  • the forked branch re-sends the same bytes raw and must re-compress from scratch (nudge starts at ~0% accumulated);
  • after compression, the branch's blocks duplicate the parent's coverage over identical content.

This is the classic radix-tree property that prefix-affinity stops short of: identity is content-addressed, but compression state is not — it lives per session (kernel StateStore, blocks owned by one session id).

Proposal (discussion)

Make the fold state effectively tree-shaped so a fork inherits the folds anchored inside its shared prefix:

  1. On fork detection (existing lineage machinery), copy/adopt the parent session's blocks whose effectiveMessageIds fall entirely inside the shared prefix (items 0..lcp-1) into the new session, remapping anchors.
  2. Keep per-session ownership for persistence — this is a one-time adoption at fork time, not shared live state (avoids two branches mutating one block).
  3. Reuse the existing per-item hash chain as the prefix-addressing mechanism (MAX_STORED_ITEMS = 128 currently caps it — may need raising or a separate block-anchor index keyed by item hash).
  4. Safety unchanged: matching requires byte-identical history possession (会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286/# [Bug/Regression] 0.1.59 关闭内容指纹会话兜底 (#286):无会话头第三方 harness 请求被直接 400,不转发上游 #309 argument).

Open questions

  • Cost/benefit: how often do real clients fork (edited history / rewind)? omp rewind, codex /compact-adjacent flows, web UIs with regenerate.
  • Interaction with restoreOutputBudget-style per-session metadata (must NOT inherit).
  • Kernel support needed: block adoption/remap API (current kernel owns blocks per session state).

Spawned from the #619 discussion — orthogonal to its remaining decode-fail scope (PR #622).

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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions