(text: string)
| 131 | * block, never on incidental repetition of a sentence or a heading. |
| 132 | */ |
| 133 | export function dedupeSelfRepeatedText(text: string): string { |
| 134 | const raw = text ?? ''; |
| 135 | if (raw.length < 400) return raw; // too short to contain a full double-answer |
| 136 | |
| 137 | // Find candidate restart points: a later occurrence of the text's own opening. |
| 138 | // Use the first ~60 non-space chars as a fingerprint of the answer's start. |
| 139 | const norm = (s: string) => s.replace(/\s+/g, ' ').trim(); |
| 140 | const head = raw.slice(0, Math.min(raw.length, 4000)); |
| 141 | const fpLen = 60; |
| 142 | const fingerprint = norm(head).slice(0, fpLen); |
| 143 | if (fingerprint.length < fpLen) return raw; |
| 144 | |
| 145 | // Search for the fingerprint appearing AGAIN after the first ~quarter of text. |
| 146 | const normFull = norm(raw); |
| 147 | const secondIdxNorm = normFull.indexOf(fingerprint, Math.floor(normFull.length * 0.25)); |
| 148 | if (secondIdxNorm === -1) return raw; |
| 149 | |
| 150 | // Map the normalized split point back to a raw offset by walking raw and |
| 151 | // counting normalized chars. Cheaper approach: split raw on a literal search of |
| 152 | // the original (un-normalized) opening line, which is what actually repeats. |
| 153 | const openingLine = raw.split('\n').find(l => l.trim().length >= 12)?.trim(); |
| 154 | if (!openingLine) return raw; |
| 155 | const firstAt = raw.indexOf(openingLine); |
| 156 | const secondAt = raw.indexOf(openingLine, firstAt + openingLine.length); |
| 157 | if (secondAt === -1) return raw; |
| 158 | |
| 159 | const first = raw.slice(firstAt, secondAt).trim(); |
| 160 | const second = raw.slice(secondAt).trim(); |
| 161 | // Confirm it's a real restart. The two copies are often NOT byte-identical — |
| 162 | // e.g. the first ends with "Would you like me to…?" and the second doesn't, or |
| 163 | // one is truncated. So instead of requiring exact containment, measure how much |
| 164 | // of a shared opening they have: split each into normalized words and count the |
| 165 | // common leading run. A genuine re-emit shares a long prefix; two different |
| 166 | // sections that merely reuse one opening line diverge almost immediately. |
| 167 | const ns = norm(first), nl = norm(second); |
| 168 | if (ns.length < 200 || nl.length < 200) return raw; |
| 169 | const wa = ns.split(' '); |
| 170 | const wb = nl.split(' '); |
| 171 | let common = 0; |
| 172 | const max = Math.min(wa.length, wb.length); |
| 173 | while (common < max && wa[common] === wb[common]) common++; |
| 174 | // Require the shared opening run to be at least 60% of the shorter copy's word |
| 175 | // count — strong evidence the model restarted the same answer, tolerant of a |
| 176 | // differing tail. |
| 177 | const sharedRatio = common / Math.min(wa.length, wb.length); |
| 178 | if (common >= 30 && sharedRatio >= 0.6) { |
| 179 | // Keep the prefix before the first copy (if any) + the longer single copy. |
| 180 | const preamble = raw.slice(0, firstAt).trim(); |
| 181 | const body = first.length >= second.length ? first : second; |
| 182 | return preamble ? `${preamble}\n\n${body}` : body; |
| 183 | } |
| 184 | return raw; |
| 185 | } |
| 186 | |
| 187 | // Re-export so the headless module can keep its single import surface. |
| 188 | export { StreamDisplayFilter }; |
no test coverage detected