abd85a94bc
`truncate_before_user_ordinal` is an index into the list of *real* user turns. The gateway builds that list with `role == "user" and not display_kind`, and `test_prompt_submit_truncate_ordinal_skips_display_kind_rows` already pins why: "Without the filter, a trailing marker shifts the ordinal so the wrong message is targeted for truncation." `_apply_personality_to_session` broke that invariant at the producer. Its pivot marker rides as `role=user` — deliberately, so strict OpenAI-compatible providers accept it mid-conversation (the same reason `_append_model_switch_marker` does) — but unlike the model-switch marker it carried no `display_kind`. The gateway therefore counted it as a real user turn while no client ever renders it as one. After a personality change the two sides address different lists: every later rewind/edit/regenerate resolves one slot too early, and `replace_messages()` hard-DELETEs the extra span. That is the reported signature — an in-range, valid ordinal, `confirm_truncate: true`, and a cut that moved backwards with no user rewind action. Tag the pivot like the model-switch marker, and teach the desktop to project the kind as a timeline row so a persisted marker is never rendered — or counted — as a user turn on the client side either. Both ends must exclude it; excluding it on only one end just inverts the drift. The regression test drives the real injection point rather than a hand-written marker dict. Without the fix it fails with "the pivot shifted the ordinal: the cut landed at 3 instead of 5", losing a turn the user never asked to drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>