fix(mobile-native-chat): stop the span cap stranding a long glued run

Capping each match attempt at 8 segments did not truncate a longer glue, it
rejected it outright: a row spelling 9+ sends exhausted the loop without
reaching the end of the text and returned zero, so none of the nine retired —
and each stuck send then inflated `earlierOutstanding` for the next send of the
same text. Nothing bounds how many sends pile onto the agent's input line;
accumulation ends when the agent accepts input again, not at any fixed count.

One inspection budget now covers the whole slide instead. The first attempt
spans the entire run and always fits, so a genuine glue is never truncated;
only a run of identical prefix-matching sends can exhaust the budget, which is
exactly the case that should be cheap. The in-flight attempt may overshoot the
remainder — that is what makes the guarantee hold — so the budget test asserts
the real ceiling. Re-fuzzed at 250k seeds with runs past the budget.
This commit is contained in:
Brennan Benson
2026-08-16 22:05:22 -07:00
parent db003d66e0
commit 8df86a56ed
2 changed files with 36 additions and 18 deletions
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import type { MobileNativeChatPendingMessage } from './mobile-native-chat-pending-echo'
import {
MAX_GLUE_SPAN,
GLUE_SLIDE_BUDGET,
retireLandedMobileNativeChatPending,
selectGluedPendingIds
} from './mobile-native-chat-pending-retirement'
@@ -201,6 +201,15 @@ describe('selectGluedPendingIds', () => {
expect(retiredIds(messages, [stuckHead, ...pair])).toEqual(['p1', 'p2'])
})
// Nothing bounds how many sends pile onto the agent's input line, so the
// slide's budget must never truncate a genuine glue.
it('retires a glued run far longer than the slide budget', () => {
const words = Array.from({ length: 12 }, (_, index) => `w${index}`)
const messages = [assistantTurn('m1', 'ready', 1000), userTurn('m2', words.join(' '), 5000)]
const pending = words.map((word, index) => pendingSend(`p${index}`, word, 'm1'))
expect(retiredIds(messages, pending)).toHaveLength(words.length)
})
it('skips a caption-less image echo instead of gluing it', () => {
const messages = [assistantTurn('m1', 'ready', 1000), userTurn('m2', 'one two', 5000)]
const pending = [
@@ -229,8 +238,10 @@ describe('selectGluedPendingIds', () => {
})
// The cursor slides past a head that cannot match, so a turn may try several
// start positions — but each try is capped at MAX_GLUE_SPAN segments, which
// keeps the work linear in the run length rather than quadratic.
// start positions — but one inspection budget covers the whole slide, keeping
// the work linear in the run length rather than quadratic. The attempt already
// in flight is allowed to overshoot the remaining budget, deliberately: that is
// what guarantees a genuine long glue is never truncated.
it('keeps segment inspection linear in the run length per transcript turn', () => {
const pendingCount = 96
const turnCount = 12
@@ -250,7 +261,7 @@ describe('selectGluedPendingIds', () => {
} finally {
startsWith.mockRestore()
}
expect(segmentChecks).toBeLessThanOrEqual(turnCount * pendingCount * MAX_GLUE_SPAN)
expect(segmentChecks).toBeLessThanOrEqual(turnCount * (2 * pendingCount + GLUE_SLIDE_BUDGET))
})
})
@@ -8,10 +8,13 @@ import type { MobileNativeChatPendingMessage } from './mobile-native-chat-pendin
const SPACE = ' '
const NO_PENDING_IDS: ReadonlySet<string> = new Set()
// The host shares one input line, not an unbounded one: real glue is two or
// three sends. Capping the span keeps the cursor slide below linear-per-turn
// instead of quadratic when a long run of identical sends is outstanding.
export const MAX_GLUE_SPAN = 8
// Slack the cursor slide may spend re-trying later start positions, on top of
// one free pass over the run. Nothing bounds how many sends accumulate on the
// agent's input line — that ends when the agent accepts input again — so the
// budget must never truncate a genuine glue: the first attempt covers the whole
// run and always fits. It only stops a run of identical prefix-matching sends
// from making the scan quadratic.
export const GLUE_SLIDE_BUDGET = 8
type UserTurn = { index: number; text: string }
type GlueSegment = { text: string; tail: number } | null
@@ -71,10 +74,13 @@ export function selectGluedPendingIds(
// otherwise disable glue retirement for every later pair, for the rest of
// the session. Slide past it; the cursor stays monotonic, so a later turn
// can never claim a send an earlier one already took.
let budget = runEnd - runStart + GLUE_SLIDE_BUDGET
let start = cursor
let matched = 0
for (; start <= runEnd - 2; start++) {
matched = matchGluedRun(turn, segments, start, runEnd)
for (; start <= runEnd - 2 && budget > 0; start++) {
const attempt = matchGluedRun(turn, segments, start, runEnd)
budget -= attempt.inspected
matched = attempt.matched
if (matched > 0) {
break
}
@@ -92,37 +98,38 @@ export function selectGluedPendingIds(
return retired
}
/** Length of the exact glued run at `start`, or zero. */
/** Length of the exact glued run at `start`, plus the segments it had to read. */
function matchGluedRun(
turn: UserTurn,
segments: readonly GlueSegment[],
start: number,
end: number
): number {
): { matched: number; inspected: number } {
let at = 0
let matched = 0
const limit = Math.min(end, start + MAX_GLUE_SPAN)
for (let index = start; index < limit; index++) {
let inspected = 0
for (let index = start; index < end; index++) {
const segment = segments[index]!
inspected += 1
// Every send carries its OWN boundary: a row that already existed when this
// send was issued can never be part of its echo, however well it reads.
if (turn.index <= segment.tail) {
return 0
return { matched: 0, inspected }
}
if (at > 0 && turn.text[at] === SPACE) {
at += 1
}
if (!turn.text.startsWith(segment.text, at)) {
return 0
return { matched: 0, inspected }
}
at += segment.text.length
matched += 1
if (at === turn.text.length) {
// A lone exact match is an ordinary landing, which the count pass owns.
return matched > 1 ? matched : 0
return { matched: matched > 1 ? matched : 0, inspected }
}
}
return 0
return { matched: 0, inspected }
}
/** Retires exact and glued transcript landings while preserving pending order. */