Repository navigation
fix(desktop): branch cuts the parent transcript at a durable row id - #128550
OutThisLife wants to merge 3 commits into
Conversation
૮ >ﻌ< ა ci reviewran on c263fba — test(desktop): drop the duplicate routedSessionId in the war debug infoCI timingsCI timings · View report · View jobWall time 15m13s vs 33m52s (-55.1%). 4 job(s) slower, 12 faster,
|
|
Can we pleasssseee get this fixed, branching is broken and i don't understand why this isn't a higher priority, i seriously can't be the only one who is constantly having this issue |
|
@teknium1 @kshitijk4poor please review <3 thank you <3 |
25b4419 to
3f35635
Compare
session.branch truncated the parent's live history with history[:count], where count came from the desktop's toBranchMessages() over the MERGED toChatMessages projection (tool rows folded into assistant bubbles, empty tool-call turns collapsed, timeline markers interleaved). That merged-bubble count has no stable mapping onto the backend's raw row history, so every desktop branch silently sliced off the conversation tail — a 261-row parent forked only its first ~108 rows (#80973, partially #87949). Two more defects a pure count -> row-id swap would not fix: 1. Merged bubbles span several rows. An auto-continued long answer is persisted as multiple assistant rows plus folded tool rows, but the bubble only carried the FIRST row's rowId. Cutting at that id would still drop the bubble's own tail, so bubbles now carry endRowId — the last durable row of the span they cover — and the cut uses it. 2. Nested branches addressed the wrong database. A branch child starts with its parent's ChatMessage objects (parent row ids) and its live history kept the parent's _row_ids after the copy. A second-generation fork then sent an id that exists in the child's REST transcript but not in its live history. The copy now re-stamps the in-memory history with the child's new SQLite ids (read back with include_row_ids). Backend — session.branch (tui_gateway/methods_session.py) - Whole raw history is copied by default; truncation happens only via up_to_row_id (durable messages.id). - The cut is ORDINAL ("keep rows whose id <= target"), not an exact-member match: a merged bubble's terminal id may belong to a row this projection filtered out (folded tool row, hidden marker), and requiring that exact row would wrongly reject the fork. Ids are monotonically increasing in insertion order, so the ordinal cut lands exactly on the last visible row of the clicked bubble's span. - Id-range guard: a target outside [min, max] of the ids this transcript ever persisted is refused with 4009 branch target row not found — a stale/foreign id can never silently broaden the fork into a full-history branch (the exact failure mode the count-based path had). - After the copy, the in-memory history is re-stamped with the child's new row ids so nested branches resolve against the child's own database. Frontend (desktop) - toChatMessages hydration stamps endRowId on every bubble: continuation rows merged into an active assistant, tool results folded via applyStoredToolResult, and pending tool flushes. rowId keeps naming the first row (reactions still address it). - branchCurrentSession addresses the cut with the terminal bubble's endRowId ?? rowId; a fresh turn that has not round-tripped row ids falls back to resolving against the REST transcript (resolveDurableRowIdForMessage, same-role/text ordinal matching), and if the cut still cannot be addressed the fork is refused with an error instead of silently retargeting. - Whole-chat forks send NO truncation at all; the backend copies everything. - The cut address is part of the branch create-flight identity, so two branches taken at different messages of the same live parent are not coalesced. Contract - session.branch params gain up_to_row_id; the legacy merged-message count is kept in the schema for older senders but IGNORED by the handler. - DESKTOP_BACKEND_CONTRACT / REQUIRED_BACKEND_CONTRACT -> v8 (a v7 backend rejects the new parameter, so the desktop warns to align rather than failing cryptically), with the generated contract artefacts regenerated. Test plan - scripts/run_tests.sh tests/tui_gateway/test_tui_gateway_server.py — 669 passed (covers default full copy, ordinal cut, id-range refusal, nested-branch re-addressing) - vitest use-session-actions + lib/chat-messages + store/updates — 421 passed - tsc --noEmit clean Rebased onto current main (2026-09-20) by @hutao562: ported to the split tui_gateway modules, added the RPC parameter to the gateway contract + bumped the GUI/backend contract to v8, regenerated the contract artefacts, folded the cut address into the create-flight identity, and updated the regression tests.
3f35635 to
c263fba
Compare
|
Branch-contract review: moving the cut from merged-message count to durable |
|
Data point for a variant of #80973 that this PR should cover: a transcript containing a notice row (e.g. `model_switch`, persisted as `role=user` + `display_kind`) also breaks the count cut. The renderer turns it into a `role: 'system'` / `systemNotice` message that `toBranchMessages` filters out, while `_visible_branch_history` keeps it, so `history[:count]` lands one row early and "Branch in new chat" on the last reply drops it. Measured on a real session: parent 17 visible rows, child 16. From reading the diff, the ordinal `_row_id <= up_to_row_id` cut keeps notice rows regardless of how the renderer counts, so this should be fixed here. I haven't run the branch. Might be worth a regression case with a notice row before the clicked message. |
|
I rebased this PR on If it helps, you can take these commits into this PR. The first two commits come from this PR, and their authors do not change. The third PR commit is not necessary now (see below). The message of
Fixes
Code health. The ratchet became blocking on 2026-10-05, after the last CI run of this PR. The PR made 8 units larger than their caps. These commits move code and tests. They do not change behavior, and they add no
Tests
Real session (Linux desktop build)
Related
|
fix(desktop): branch cuts the parent transcript at a durable row id
Root cause
The desktop sent a merged-bubble count and the backend sliced raw rows with
history[:count]; the two count spaces have no stable mapping (tool rows foldinto assistant bubbles, compaction replaces the live projection), so the cut
landed early and silently dropped the conversation tail. A second-generation
branch failed outright ("row not found") because the child kept the parent's
row ids.
What changes
Consolidates the two contributor approaches, contract-first:
@hutao562):session.branchparams gainup_to_row_id(OpenRPC + regenerated TS); the legacy
countis documented as IGNORED.The desktop resolves the clicked bubble's terminal durable row id
(
resolveDurableRowIdForMessage, with a REST-transcript fallback) andrefuses the branch when the cut target cannot be resolved — no live-atom
guess, no silent early cut.
@kingOfSoySauce, adapted):session.branchtruncates byup_to_row_idand errors 4009 on an unknown row; the copied child rows arere-addressed with their NEW child-db row ids (
append_messages_batchreturn ids re-stamped into the live history), so a second-generation branch
can address its cut.
(
session.branch_whole), untouched.Acceptance coverage
#80973: whole-chat branch copies the parent's complete raw history (count
mismatch ignored); fork-from-message cuts at the clicked bubble's exact raw
row; child keeps valid row ids (second-generation branch works).
#87949: message-level branch of a compacted long session copies the full
persisted history up to the cut — new regression test
test_session_branch_compacted_parent_cuts_on_persisted_rows(614-row parentcompacted to summary+tail, branched at a mid-archived-region bubble, asserts
the child sees the persisted rows).
Tests (all real runs)
HERMES_PYTHON=… bash scripts/run_tests.sh -j 2 --file-timeout 1500 tests/tui_gateway/test_tui_gateway_server.py -k "branch"— 13/13 passed athead
6f0da767e7. Red onorigin/main(40e4af42b8e, test file copiedin): 2 failed (
test_session_branch_full_history_by_default_truncates_by_row_id,test_session_branch_compacted_parent_cuts_on_persisted_rows).cd apps/desktop && npx vitest run --maxWorkers=2 src/app/session/hooks/use-session-actions.test.tsx src/app/session/hooks/use-session-actions/utils.test.ts— 266/266 passedat head. Red on
origin/main: 3 failed (resolves the matching occurrence when live messages have ephemeral ids,returns undefined when the selected message is not in the persisted transcript,branches from a specific message by durable row id).Note: #80970's orphaned-tool-call pairing guard was evaluated and dropped —
main's display-projection branch source (
_visible_branch_history) filterstool rows out of branch copies entirely, so the guard can never fire on this
path. Its Electron packaging files were never carried. #80970's tile-fork
wiring is superseded separately by #128549 (the focused tile-fork PR), per the
routing verdicts.
Fixes #80973
Fixes #87949
Supersedes #98025
Supersedes #80970
Thanks @hutao562 — the contract-first commit (38ac027) is theirs and is the
base of this branch. Thanks @kingOfSoySauce — the backend row-id restamping
and 4009 guard are ported from #80970.
Co-authored-by: Leon 1172300059@qq.com