1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! Split persisted assistant content back into its chronological segments.
//!
//! The tool loop persists a turn one iteration at a time, each append being a
//! `<!-- reasoning -->` block followed by that iteration's visible text. Reload
//! used to split only on TOOL markers, so a turn that ran no tools collapsed
//! every iteration into ONE assistant message: think, text, think, text became
//! a single row holding all the text and one merged `details` blob.
//!
//! That single row is then the turn's last non-empty assistant message, which
//! makes it the final answer, which the per-turn fold must always show. A
//! reasoning-heavy turn therefore rendered as a wall after a restart while the
//! live view, where each iteration was its own row, had folded it away.
//!
//! Splitting on the reasoning markers restores the live layout, and it works on
//! rows already in the database because it changes no storage format.
//!
//! A marker counts only at the start of a line, which is how the persist
//! path writes every one of them. The model quoting a marker mid-sentence
//! (it reads its own history through `session_search`) used to close a real
//! block early and turn the rest of the reasoning into the answer (#1587).
/// One chronological piece of a persisted assistant message.
pub
use find_at_line_start;
const OPEN: &str = "<!-- reasoning -->";
const CLOSE: &str = "<!-- /reasoning -->";
const BLOCKED_OPEN: &str = "<!-- phantom_blocked=1 -->";
const BLOCKED_CLOSE: &str = "<!-- /phantom_blocked=1 -->";
/// Heading a collapsed blocked section carries, so an expanded row says what
/// it is instead of reading like an answer that was hidden by mistake.
pub const BLOCKED_LABEL: &str = "Blocked narration (phantom tool calls, never delivered):";
/// Split `content` into text, reasoning and blocked segments, in order.
///
/// Empty pieces are dropped, so a turn whose iterations were pure reasoning
/// yields only `Reasoning` segments and never a blank row. An unclosed block
/// takes the rest of the input as reasoning: the alternative is dumping a
/// half-written chain into the visible answer, which is the exact failure this
/// exists to prevent.
///
/// Phantom-blocked sections are cut out first (#1584). The split used to know
/// only the reasoning markers, so a blocked section came apart into its
/// opener line, its reasoning, and its narration plus closer as the last text
/// segment, which is the final answer by position. The 21K-character wall the
/// self-heal had drained from the live view then rendered in full on the next
/// reload, closing marker and all. An unclosed opener folds to the end of the
/// row for the same reason an unclosed reasoning block does.
pub
/// Flatten a blocked section into one string: its reasoning and narration in
/// order, markers gone, so the collapsed row shows what the model produced.
/// Append `content` to `out` as alternating text and reasoning segments.
/// Push visible text, minus any line that is nothing but a marker: an
/// orphaned close the model quoted, or the half of a pair a truncated write
/// left behind. Such a line is never content, and shown raw it reads as a
/// leak (#1587).
/// Whether the text segment at `i` is intermediate narration rather than the
/// turn's answer.
///
/// A text segment with reasoning after it AND more text behind that reasoning
/// is one the model kept thinking past before writing on, so it cannot be the
/// answer however much it reads like one. When nothing but reasoning follows,
/// the text is the answer.
///
/// The distinction is the CLI persist layout (#1728): the CLI branch appends a
/// turn's whole reasoning as one block at EOF, AFTER the streamed answer, so
/// every CLI reasoning turn ends `[answer, reasoning]` even though the answer
/// was fully delivered. Demoting on reasoning alone hid that answer behind a
/// collapsed details row the moment the session reloaded, while the live view
/// had shown it in full. A reasoning block with no text behind it is
/// therefore a terminal append artifact, not proof the model was still
/// thinking.
///
/// Reasoning BETWEEN texts still demotes the earlier text (#760): the model
/// sometimes restates its reasoning through the content channel, where it is
/// persisted unwrapped and is otherwise indistinguishable from the answer.
/// Position is the one signal that stays exact, so it is the one used here: no
/// guessing at which prose looks like thinking.
///
/// The cost of the terminal-reasoning exception is a truncated turn (narration
/// persisted, crash before the answer) rendering its narration as content
/// instead of collapsing it. Position alone cannot tell a truncated turn from
/// the CLI artifact, the artifact ships on every CLI reasoning turn while the
/// truncation is a crash corner, and erring visible matches the live view.
///
/// A blocked section counts for nothing here: it is never the answer itself,
/// and the reasoning it folded in must not demote the real answer before it.
pub