Skip to content

feat(threads): distinguish branched conversations - #4983

Merged
WillemJiang merged 4 commits into
bytedance:mainfrom
fancyboi999:fancy/4964-branch-title-suffix
Aug 25, 2026
Merged

feat(threads): distinguish branched conversations#4983
WillemJiang merged 4 commits into
bytedance:mainfrom
fancyboi999:fancy/4964-branch-title-suffix

Conversation

@fancyboi999

@fancyboi999 fancyboi999 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4964

Why

The branch endpoint inherited the source thread title unchanged, so the original conversation and its branch appeared with identical labels in the flat recent-chat list. Even after disambiguating titles, the Sidebar did not expose the existing parent/child relationship carried in thread metadata.

A persisted Branch: / 分叉: label would make titles depend on the UI locale. Numeric suffixes keep the topic first, while a lightweight tree projection makes the relationship visible without introducing a second lineage API or collapsible UI state.

What changed

  • Automatically inherited branch titles use the next free displayed sibling suffix: repeated branches of Title become Title (2), Title (3), and so on, while matching explicit or renamed sibling titles reserve the same suffix without needing generated metadata.
  • Branch creation holds a durable source-thread reservation while it reads sibling metadata and creates the child, preventing concurrent Gateway workers from issuing the same generated sibling title.
  • Automatic sequence metadata is stored only for generated suffixes. A manual rename atomically removes the stale generated sequence, so branching a renamed Report Q4 starts at Report Q4 (2).
  • The title helper returns the display name and sequence together, keeping checkpoint titles and thread metadata on one derivation path.
  • Memory and SQL thread searches share JSON type semantics: missing differs from null, booleans differ from integers, float filters accept JSON integer/real values, and SQL compilation cache keys include the filter type.
  • Explicit branch titles remain unchanged, legacy Branch: prefixes are normalized, and generated titles stay within the 256-character storage limit.
  • The selected title is written to both thread metadata and branch checkpoints, preventing the next run from restoring the source title.
  • Recent chats projects already-loaded branch_parent_thread_id metadata into parent → child order with subtle ├─ / └─ connectors and localized accessible labels.
  • Unpinned groups follow their freshest descendant; pinned root order remains stable. Missing, malformed, cross-pin, self-parented, and cyclic lineage stays visible at the top level.
  • Recursive order is preserved while visual indentation is capped, so deep branches do not consume the Sidebar's title width.
  • Backend route/store coverage, pure frontend positive/negative tests, E2E mocks, and English/Chinese documentation describe the same behavior.

Surface area

  • Frontend UI — page / component / setting / interaction under frontend/
  • Backend API — endpoint / SSE event / request-response shape under backend/app
  • Agents / LangGraph — agent node, graph wiring, langgraph.json, or prompt change
  • Sandboxdocker/ or sandboxed execution
  • Skills — change under skills/
  • Dependencies — new/upgraded entry in backend/pyproject.toml or frontend/package.json (say what it buys us)
  • Default behavior change — changes existing behavior without the user opting in (default model, default setting, data shape)
  • Docs / tests / CI only — no runtime behavior change

Screenshots / Recording

The screenshots use an isolated SQLite database containing only the verification parent, two child branches, and one nested branch.

Desktop

Recent chats branch lineage on desktop

Mobile

Recent chats branch lineage in the mobile Sidebar

Validation

  • cd backend && make lint — passed (ruff check, ruff format --check; 1207 files checked)
  • cd backend && uv run pytest tests/test_threads_router.py tests/test_thread_meta_repo.py tests/test_memory_thread_meta_isolation.py tests/test_agent_guidance_check.py -q — 160 passed
  • Real Gateway HTTP chain on SQLite — created one parent and two automatic sibling branches, verified Original chat (2) / Original chat (3) plus stored sequences 2 / 3; renamed the first child to Report Q4, verified its generated sequence was removed, then verified its next branch was Report Q4 (2). All four verification threads were deleted and the Gateway was stopped.
  • Review follow-up HTTP/store chain — an explicit Original chat (2) sibling carried no generated sequence, and the next automatic sibling became Original chat (3). Ordered true → 1 → 1.0 → null filters returned boolean, integer, integer+float, and explicit-null rows respectively in both SQLite Gateway requests and the memory ThreadMetaStore. All eight verification threads were deleted and the Gateway was stopped.
  • cd frontend && pnpm lint && pnpm typecheck — passed
  • Changed frontend files pass targeted Prettier checks
  • cd frontend && pnpm test — 1034 passed across 134 files, 0 skipped
  • cd frontend && pnpm test:e2e tests/e2e/branch-thread.spec.ts tests/e2e/thread-list-pin.spec.ts tests/e2e/thread-list-infinite-scroll.spec.ts --project=chromium — 7 passed
  • cd frontend && BETTER_AUTH_SECRET=local-dev-secret pnpm build — production build passed; 101 static pages generated
  • Impeccable UI detector — no findings
  • Real product chain: started Gateway with an isolated SQLite config and Next.js locally, created a parent, sibling branches, and a nested branch through HTTP, and verified the actual Sidebar in Chromium at 1280×800 and 390×844. Both viewports preserved exact parent/depth/order metadata, had no horizontal overflow, and reported zero browser console errors. All verification threads were deleted and read back as absent.

AI assistance

Tool(s) used: Codex

How you used it: Investigated DeerFlow and Hermes branch-list behavior, implemented title and Sidebar lineage handling, wrote positive/negative and E2E regression tests, updated documentation, and ran the validation listed above.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ area:docs Documentation and Markdown only area:frontend Next.js frontend under frontend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/M PR changes 100-300 lines size/L PR changes 300-700 lines and removed size/M PR changes 100-300 lines labels Aug 24, 2026
@fancyboi999 fancyboi999 changed the title feat(threads): number branched conversation titles feat(threads): distinguish branched conversations Aug 24, 2026

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at head c2da41e. Overall this is a careful change - the lineage projection is defensive (missing/malformed/cross-pin/cyclic parents stay top-level, defense-in-depth emit pass), the backend sequence metadata is validated against forged values, tests cover the positive/negative/E2E surface, and docs are updated per convention. Findings below are suggestion-level gaps in the numeric-suffix scheme rather than blocking issues: repeated branching from the same source can still produce duplicate Title (2) siblings, and a user rename leaves stale branch_title_sequence metadata that makes the next auto suffix skip numbers.

Comment thread backend/app/gateway/routers/threads.py
Comment thread backend/app/gateway/routers/threads.py Outdated
Comment thread backend/app/gateway/routers/threads.py
@WillemJiang WillemJiang added this to the 2.1.0 milestone Aug 24, 2026
@fancyboi999
fancyboi999 force-pushed the fancy/4964-branch-title-suffix branch from c2da41e to ad87332 Compare August 24, 2026 08:24
@github-actions github-actions Bot added size/XL PR changes 700+ lines and removed size/L PR changes 300-700 lines labels Aug 24, 2026
@fancyboi999
fancyboi999 force-pushed the fancy/4964-branch-title-suffix branch from ad87332 to dba722d Compare August 24, 2026 08:26

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Count explicitly titled siblings when allocating suffixes

The allocator considers a suffix occupied only when the sibling has valid branch_title_sequence metadata. Explicitly titled branches do not have that metadata, and manual renames intentionally remove it. Therefore, if a sibling is explicitly titled or renamed to Original chat (2), the next automatic branch also becomes Original chat (2), recreating the ambiguity this PR addresses.

Occupancy should be determined from sibling display names regardless of whether the suffix was generated. Sequence metadata can still determine the starting point when branching from a numbered source. Please add coverage for an explicit or renamed Title (2) sibling followed by an automatic branch.

[P2] Preserve exact JSON matching in memory-mode searches

The new client-side predicate uses metadata.get(key) == value, which differs from the SQL store type-safe matching:

  • Searching for {"tag": null} also matches records where tag is absent.
  • Searching for {"flag": true} can match {"flag": 1} because Python considers True == 1.

I reproduced the first case: the memory store returned both the explicit-null and missing-key records, while SQLite returned only the explicit-null record. The predicate should require key presence and preserve JSON boolean/number distinctions, ideally through a shared matcher and cross-store contract tests.

Validation on commit dba722d2:

  • Focused backend suite: 145 passed
  • New frontend tree suite: 5 passed
  • Diff check: clean
  • Current CI checks: green

@fancyboi999
fancyboi999 force-pushed the fancy/4964-branch-title-suffix branch from dba722d to a133094 Compare August 24, 2026 11:08
@fancyboi999

Copy link
Copy Markdown
Collaborator Author

Addressed both requested changes in a1330949.

  • Suffix occupancy now uses exact sibling display_name values rather than requiring branch_title_sequence. Explicit or renamed Original chat (2) therefore reserves (2), and the next automatic sibling becomes (3). Route coverage also pins an explicit (4) followed by automatic (5).
  • Memory-mode metadata searches now use a shared type-aware JSON matcher. Missing/null, bool/int, and int/float behavior is covered against the SQL contract.
  • SQLAlchemy's JsonMatch cache key now includes the filter value type, preventing True, 1, and 1.0 from reusing one another's compiled predicate.

Validation: backend lint/format passed; the focused router/store/guidance suite reports 160 passed; real SQLite Gateway and memory-store runs produced the same ordered JSON-filter results. Temporary verification threads were removed.

@WillemJiang
WillemJiang merged commit 943d148 into bytedance:main Aug 25, 2026
17 checks passed
LittleChenLiya added a commit to LittleChenLiya/deer-flow that referenced this pull request Aug 25, 2026
Keep the redesigned memory facts list from this PR. Main's Streamdown sanitization on memory summaries does not apply because this branch no longer renders summaries via SafeStreamdown. Auto-merged chat-page (ThreadSubagentBatches), i18n, and mock-api branch coverage from bytedance#4983/bytedance#4998/bytedance#4987.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ area:docs Documentation and Markdown only area:frontend Next.js frontend under frontend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/XL PR changes 700+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 分叉对话,添加:“分叉” 前缀

3 participants