feat(threads): distinguish branched conversations - #4983
Conversation
willem-bd
left a comment
There was a problem hiding this comment.
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.
c2da41e to
ad87332
Compare
ad87332 to
dba722d
Compare
willem-bd
left a comment
There was a problem hiding this comment.
[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 wheretagis absent. - Searching for
{"flag": true}can match{"flag": 1}because Python considersTrue == 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
dba722d to
a133094
Compare
|
Addressed both requested changes in
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. |
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.
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
TitlebecomeTitle (2),Title (3), and so on, while matching explicit or renamed sibling titles reserve the same suffix without needing generated metadata.Report Q4starts atReport Q4 (2).Branch:prefixes are normalized, and generated titles stay within the 256-character storage limit.branch_parent_thread_idmetadata into parent → child order with subtle├─/└─connectors and localized accessible labels.Surface area
frontend/backend/applanggraph.json, or prompt changedocker/or sandboxed executionskills/backend/pyproject.tomlorfrontend/package.json(say what it buys us)Screenshots / Recording
The screenshots use an isolated SQLite database containing only the verification parent, two child branches, and one nested branch.
Desktop
Mobile
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 passedOriginal chat (2)/Original chat (3)plus stored sequences2/3; renamed the first child toReport Q4, verified its generated sequence was removed, then verified its next branch wasReport Q4 (2). All four verification threads were deleted and the Gateway was stopped.Original chat (2)sibling carried no generated sequence, and the next automatic sibling becameOriginal chat (3). Orderedtrue → 1 → 1.0 → nullfilters 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— passedcd frontend && pnpm test— 1034 passed across 134 files, 0 skippedcd 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 passedcd frontend && BETTER_AUTH_SECRET=local-dev-secret pnpm build— production build passed; 101 static pages generatedAI 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.