Agent Manager commited on
Commit
b7e3caa
·
1 Parent(s): 3d5074b

Reject weak repaint history anchors

Browse files
Files changed (2) hide show
  1. server/resize.test.mjs +17 -2
  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
- const restarted = await view(id, 150, 40, true);
422
- const resumed = await waitFor(async () => (await gridText(id)).includes('[fixture 150x40]'));
 
 
 
 
 
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
- const match = candidate ? longestPrefixOccurrence(candidate, archive) : { length: 0, end: -1 };
301
- const minimum = Math.min(12, candidate.trim().length, archive.trim().length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;