fix: close sibling display_kind drops and ui-tui parity for #82756
Review follow-ups on the composite salvage (whole-bug-class sweep): - session.branch and _persist_branch_seed copied parent history without display_kind/display_metadata, so a tagged timeline marker (personality pivot, model switch, auto-continue) re-entered the branched session as a bare role=user row after a restart — re-planting the phantom-ordinal class this PR fixes. Both projection dicts now carry the tags; regression asserts added to both branch tests (mutation-checked: fail without the fix). - ui-tui renderer learns display_kind=personality_switch (was falling through to an opaque user bubble; desktop got the case in commit 1). - programmatic-integration docs: document the two new 4004 refusals (boolean ordinal, bare confirm_truncate). - hermes_state comment: archived rows are searchable only with include_inactive=True, not by default search — align comment with the actual FTS filter. - strip stray trailing blank line in test_tui_gateway_server.py
This commit is contained in:
parent
4d79bd3d02
commit
327f7efab8
|
|
@ -8026,9 +8026,10 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
|||
raise CompressionSessionClosedError(session_id)
|
||||
if archive_dropped:
|
||||
# Content-preserving UPDATE: the rows keep their FTS entries
|
||||
# (the messages_fts triggers index on INSERT / drop on DELETE
|
||||
# and don't key on active), so the replaced turns stay both
|
||||
# readable and searchable-by-id after the rewrite.
|
||||
# (the messages_fts triggers fire on INSERT / DELETE / UPDATE
|
||||
# of content columns, not on `active`), so the replaced turns
|
||||
# stay readable via get_messages(include_inactive=True) and
|
||||
# searchable with include_inactive=True after the rewrite.
|
||||
conn.execute(
|
||||
"UPDATE messages SET active = 0 "
|
||||
"WHERE session_id = ? AND active = 1",
|
||||
|
|
|
|||
|
|
@ -16611,9 +16611,28 @@ def _branch_history():
|
|||
"codex_reasoning_items": BRANCH_CODEX_REASONING_ITEMS,
|
||||
"codex_message_items": BRANCH_CODEX_MESSAGE_ITEMS,
|
||||
},
|
||||
# Timeline marker: rides as role=user but must keep its tag through
|
||||
# the branch copy, or it re-enters the truncate ordinal address space
|
||||
# as a phantom user turn after a restart (#82756).
|
||||
{
|
||||
"role": "user",
|
||||
"content": "[System: personality changed]",
|
||||
"display_kind": "personality_switch",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _branched_marker(db, session_key):
|
||||
return next(
|
||||
(
|
||||
m
|
||||
for m in db.get_messages_as_conversation(session_key)
|
||||
if m.get("display_kind") == "personality_switch"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _branched_assistant(db, session_key):
|
||||
return next(
|
||||
m
|
||||
|
|
@ -16650,6 +16669,11 @@ def test_persist_branch_seed_keeps_reasoning_fields(monkeypatch, tmp_path):
|
|||
assert assistant["reasoning_details"] == BRANCH_REASONING_DETAILS
|
||||
assert assistant["codex_reasoning_items"] == BRANCH_CODEX_REASONING_ITEMS
|
||||
assert assistant["codex_message_items"] == BRANCH_CODEX_MESSAGE_ITEMS
|
||||
marker = _branched_marker(db, "branch-key")
|
||||
assert marker is not None, (
|
||||
"the branch seed dropped display_kind: the marker re-entered the "
|
||||
"truncate ordinal address space as a phantom user turn (#82756)"
|
||||
)
|
||||
assert session["_branch_seed_persisted"] is True
|
||||
finally:
|
||||
db.close()
|
||||
|
|
@ -16692,6 +16716,11 @@ def test_session_branch_keeps_reasoning_fields(monkeypatch, tmp_path):
|
|||
assert assistant["reasoning_details"] == BRANCH_REASONING_DETAILS
|
||||
assert assistant["codex_reasoning_items"] == BRANCH_CODEX_REASONING_ITEMS
|
||||
assert assistant["codex_message_items"] == BRANCH_CODEX_MESSAGE_ITEMS
|
||||
marker = _branched_marker(db, "branch-key")
|
||||
assert marker is not None, (
|
||||
"session.branch dropped display_kind: the marker re-entered the "
|
||||
"truncate ordinal address space as a phantom user turn (#82756)"
|
||||
)
|
||||
finally:
|
||||
server._sessions.pop("sid", None)
|
||||
db.close()
|
||||
|
|
@ -16997,4 +17026,3 @@ def test_prompt_submit_truncation_archives_instead_of_deleting(monkeypatch):
|
|||
assert captured.get("active_only") is True
|
||||
finally:
|
||||
server._sessions.pop("archive-trunc-sid", None)
|
||||
|
||||
|
|
|
|||
|
|
@ -2825,6 +2825,13 @@ def _(rid, params: dict) -> dict:
|
|||
"reasoning_details": msg.get("reasoning_details"),
|
||||
"codex_reasoning_items": msg.get("codex_reasoning_items"),
|
||||
"codex_message_items": msg.get("codex_message_items"),
|
||||
# Timeline markers (model_switch, personality_switch,
|
||||
# auto_continue, …) ride as role=user; dropping the tag
|
||||
# here re-planted them as bare user turns after a
|
||||
# restart, corrupting the truncate ordinal address
|
||||
# space the same way #82756 did.
|
||||
"display_kind": msg.get("display_kind"),
|
||||
"display_metadata": msg.get("display_metadata"),
|
||||
# Preserve the parent's original message timestamps —
|
||||
# branch copies are history, not new activity (9d73006ad).
|
||||
"timestamp": msg.get("timestamp"),
|
||||
|
|
|
|||
|
|
@ -2894,6 +2894,13 @@ def _persist_branch_seed(session: dict) -> None:
|
|||
"reasoning_details": msg.get("reasoning_details"),
|
||||
"codex_reasoning_items": msg.get("codex_reasoning_items"),
|
||||
"codex_message_items": msg.get("codex_message_items"),
|
||||
# Timeline markers (model_switch, personality_switch,
|
||||
# auto_continue, …) ride as role=user; dropping the tag
|
||||
# here re-planted them as bare user turns after a
|
||||
# restart, corrupting the truncate ordinal address
|
||||
# space the same way #82756 did.
|
||||
"display_kind": msg.get("display_kind"),
|
||||
"display_metadata": msg.get("display_metadata"),
|
||||
# Preserve the parent's original message timestamps —
|
||||
# append_message would otherwise stamp time.time() and the
|
||||
# branch's copied history would all appear authored "now".
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ export const toTranscriptMessages = (rows: unknown): Msg[] => {
|
|||
continue
|
||||
}
|
||||
|
||||
if (display_kind === 'personality_switch') {
|
||||
out.push({ kind: 'event', role: 'system', text: 'personality changed' })
|
||||
pending = []
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (display_kind === 'async_delegation_complete') {
|
||||
const meta = (row as TranscriptRow).display_metadata
|
||||
const count = meta && typeof meta.task_count === 'number' ? meta.task_count : undefined
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ A rewind / edit / regenerate is a `prompt.submit` that drops part of the stored
|
|||
|
||||
| Parameter | Meaning |
|
||||
|-----------|---------|
|
||||
| `truncate_before_user_ordinal` | Zero-based index of the user turn to cut at. Everything from that turn onward is dropped. Display-only timeline rows (`display_kind`) are not counted. |
|
||||
| `confirm_truncate` | Required whenever an ordinal is sent. Declares that this submit really is a rewind, not an ordinary send that happens to carry a leftover ordinal. |
|
||||
| `truncate_before_user_ordinal` | Zero-based index of the user turn to cut at. Everything from that turn onward is dropped. Display-only timeline rows (`display_kind`) are not counted. Must be a real integer — a JSON boolean is refused with code `4004`. |
|
||||
| `confirm_truncate` | Required whenever an ordinal is sent. Declares that this submit really is a rewind, not an ordinary send that happens to carry a leftover ordinal. Sending it without an ordinal is refused with code `4004` (leaked rewind state). |
|
||||
| `confirm_empty_truncate` | Additionally required when the cut would leave the transcript empty (ordinal `0`). |
|
||||
|
||||
An ordinal without `confirm_truncate` is refused with code `4029` and nothing is written. Hosts that implement rewind must set the flag at the moment the user asks for it, and must never keep the ordinal in state across ordinary submits.
|
||||
|
|
|
|||
Loading…
Reference in New Issue