Spaces:
Running
Running
Agent Manager commited on
Commit ·
b7e3caa
1
Parent(s): 3d5074b
Reject weak repaint history anchors
Browse files- server/resize.test.mjs +17 -2
- server/src/runner.js +42 -2
server/resize.test.mjs
CHANGED
|
@@ -268,6 +268,16 @@ try {
|
|
| 268 |
repaintArchiveView('deep history\nshared transcript\n[old 160x40]\n',
|
| 269 |
'shared transcript\n[new 90x24]\n').archive
|
| 270 |
=== 'deep history\nshared transcript\n[new 90x24]\n');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
check('zooming wide again exposes the archive without growing it',
|
| 272 |
repaintArchiveView(narrowView.archive, wide).archive === wide);
|
| 273 |
}
|
|
@@ -418,8 +428,13 @@ try {
|
|
| 418 |
// either the old frame or its welcome banner.
|
| 419 |
const checkpoint = path.join(DATA_DIR, 'state', 'terminal-history', `${id}.json`);
|
| 420 |
check('terminal history is checkpointed durably', await waitFor(() => fs.existsSync(checkpoint)));
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
const startupCommitted = await waitFor(() => restarted.frames.some((frame) =>
|
| 424 |
frame.t === 'grid' && frame.reset));
|
| 425 |
await sleep(350);
|
|
|
|
| 268 |
repaintArchiveView('deep history\nshared transcript\n[old 160x40]\n',
|
| 269 |
'shared transcript\n[new 90x24]\n').archive
|
| 270 |
=== 'deep history\nshared transcript\n[new 90x24]\n');
|
| 271 |
+
const poem = '❯ write me a poem about silicon\n\n● Silicon\n\n'
|
| 272 |
+
+ 'Second most common thing underfoot, plain as the beach you forget while you walk it —\n'
|
| 273 |
+
+ 'Someone thought to melt it, draw it out into a single perfect column, one lattice unbroken.\n';
|
| 274 |
+
const volatilePrefix = repaintArchiveView(
|
| 275 |
+
`older unique turn\n${poem}✻ Worked for 8s\n[old footer]\n`,
|
| 276 |
+
`✻ Worked for 2s\n${poem}✻ Worked for 8s\n[new footer]\n`,
|
| 277 |
+
).archive;
|
| 278 |
+
check('a volatile leading status cannot anchor after and duplicate the repainted turn',
|
| 279 |
+
volatilePrefix.split('❯ write me a poem about silicon').length - 1 === 1
|
| 280 |
+
&& volatilePrefix.includes('[new footer]') && !volatilePrefix.includes('[old footer]'));
|
| 281 |
check('zooming wide again exposes the archive without growing it',
|
| 282 |
repaintArchiveView(narrowView.archive, wide).archive === wide);
|
| 283 |
}
|
|
|
|
| 428 |
// either the old frame or its welcome banner.
|
| 429 |
const checkpoint = path.join(DATA_DIR, 'state', 'terminal-history', `${id}.json`);
|
| 430 |
check('terminal history is checkpointed durably', await waitFor(() => fs.existsSync(checkpoint)));
|
| 431 |
+
// A mobile pane opens its socket with xterm's provisional 80x24 geometry,
|
| 432 |
+
// then reports the measured phone grid as soon as layout settles. Exercise
|
| 433 |
+
// that change while the resumed TUI is still repainting, not only after the
|
| 434 |
+
// startup transaction has committed.
|
| 435 |
+
const restarted = await view(id, 80, 24, true);
|
| 436 |
+
restarted.resize(40, 28);
|
| 437 |
+
const resumed = await waitFor(async () => (await gridText(id)).includes('[fixture 40x28]'));
|
| 438 |
const startupCommitted = await waitFor(() => restarted.frames.some((frame) =>
|
| 439 |
frame.t === 'grid' && frame.reset));
|
| 440 |
await sleep(350);
|
server/src/runner.js
CHANGED
|
@@ -297,8 +297,48 @@ export function repaintArchiveView(archive, visible, fallbackHistory = '') {
|
|
| 297 |
if (!visible) return { archive, history: logicalHistory(archive) };
|
| 298 |
const leading = visible.match(/^(?:[ \t]*\n)*/)?.[0].length || 0;
|
| 299 |
const candidate = visible.slice(leading);
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
let history = fallbackHistory;
|
| 303 |
if (match.length >= minimum && minimum > 0) {
|
| 304 |
const archiveOffset = match.end - match.length + 1;
|
|
|
|
| 297 |
if (!visible) return { archive, history: logicalHistory(archive) };
|
| 298 |
const leading = visible.match(/^(?:[ \t]*\n)*/)?.[0].length || 0;
|
| 299 |
const candidate = visible.slice(leading);
|
| 300 |
+
let match = candidate ? longestPrefixOccurrence(candidate, archive) : { length: 0, end: -1 };
|
| 301 |
+
|
| 302 |
+
// A few shared characters are not a safe repaint boundary in a substantial
|
| 303 |
+
// screen. Claude's randomized status lines, for example, all begin with
|
| 304 |
+
// "Worked for ". Anchoring a fresh screen there can retain the old turn that
|
| 305 |
+
// precedes a later status line, then append that same turn again. Require a
|
| 306 |
+
// meaningful overlap for long screens, while still accepting complete short
|
| 307 |
+
// prompts in small panes.
|
| 308 |
+
const archiveContentLength = archive.trim().length;
|
| 309 |
+
const confidence = (tail) => Math.min(
|
| 310 |
+
64,
|
| 311 |
+
archiveContentLength,
|
| 312 |
+
Math.max(12, Math.floor(tail.trim().length / 2)),
|
| 313 |
+
);
|
| 314 |
+
let minimum = confidence(candidate);
|
| 315 |
+
|
| 316 |
+
// A repaint may start with one volatile row before its stable transcript.
|
| 317 |
+
// If the first row offers only a weak match, advance by logical lines until
|
| 318 |
+
// the first substantial overlap is found. The visible grid is bounded to 500
|
| 319 |
+
// rows, and the cap keeps a maliciously fragmented screen from repeatedly
|
| 320 |
+
// scanning a large archive.
|
| 321 |
+
if (match.length < minimum) {
|
| 322 |
+
let offset = 0;
|
| 323 |
+
for (let attempts = 0; attempts < 32;) {
|
| 324 |
+
const newline = candidate.indexOf('\n', offset);
|
| 325 |
+
if (newline < 0) break;
|
| 326 |
+
offset = newline + 1;
|
| 327 |
+
const tail = candidate.slice(offset);
|
| 328 |
+
if (!tail.trim()) break;
|
| 329 |
+
const nextBreak = tail.indexOf('\n');
|
| 330 |
+
const firstLine = tail.slice(0, nextBreak < 0 ? undefined : nextBreak);
|
| 331 |
+
if (firstLine.trim().length < 12) continue;
|
| 332 |
+
attempts++;
|
| 333 |
+
const next = longestPrefixOccurrence(tail, archive);
|
| 334 |
+
const nextMinimum = confidence(tail);
|
| 335 |
+
if (next.length >= nextMinimum) {
|
| 336 |
+
match = next;
|
| 337 |
+
minimum = nextMinimum;
|
| 338 |
+
break;
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
let history = fallbackHistory;
|
| 343 |
if (match.length >= minimum && minimum > 0) {
|
| 344 |
const archiveOffset = match.end - match.length + 1;
|