Skip to content

Commit ee2a68c

Browse files
committed
fix(mothership): keep omitting bulk until an event fits one replay write
A result with more than one large object of short fields left the second one over the write limit after the first was omitted, so the event was refused and the turn ended. Omission now repeats until the event fits or nothing large enough is left; each pass removes at least 64 KiB, so it always terminates.
1 parent 388878c commit ee2a68c

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

‎apps/sim/lib/mothership/request/session/replay-compaction.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,36 @@ describe('compactStreamEvent', () => {
296296
})
297297
})
298298

299+
it('keeps omitting bulk until the event fits when more than one large object remains', () => {
300+
const manyKeys = (prefix: string, count: number) =>
301+
Object.fromEntries(
302+
Array.from({ length: count }, (_, index) => [`${prefix}-${index}`, 'v'.repeat(300)])
303+
)
304+
const event: StreamEvent = {
305+
type: 'tool',
306+
payload: {
307+
toolCallId: 'c',
308+
toolName: 'cli_workflows_state_get',
309+
executor: 'sim',
310+
mode: 'async',
311+
phase: 'result',
312+
success: true,
313+
status: 'success',
314+
output: { workflowId: 'wf-1', a: manyKeys('a', 4_300), b: manyKeys('b', 3_600) },
315+
},
316+
}
317+
318+
const compacted = payloadOf(compactStreamEvent(event))
319+
const output = toRecord(compacted.output)
320+
321+
expect(Buffer.byteLength(JSON.stringify(compacted))).toBeLessThanOrEqual(
322+
STREAM_EVENT_MAX_PAYLOAD_BYTES
323+
)
324+
expect(output.workflowId).toBe('wf-1')
325+
expect(output.a).toMatch(/^…\[omitted, [\d.]+ MB total\]$/)
326+
expect(output.b).toMatch(/^…\[omitted, [\d.]+ MB total\]$/)
327+
})
328+
299329
it('never omits identity to make room for client-executed arguments it must keep whole', () => {
300330
const args = { workflowId: 'wf-1', input: 'w'.repeat(1.5 * MB) }
301331
const event: StreamEvent = {

‎apps/sim/lib/mothership/request/session/replay-compaction.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ function omitDominantBulk(payload: Record<string, unknown>, skipKeys: ReadonlySe
168168
* only to the copy the writer delivers and persists; the caller keeps the full
169169
* event for dispatch. Long strings are cut to their head in place, so every
170170
* object keeps its shape; if that is not enough, long arrays keep their head,
171-
* and past one replay write the dominating bulk is replaced by a size note.
171+
* and past one replay write each dominating bulk is replaced by a size note
172+
* until the event fits.
172173
* Assistant text, file previews, and the arguments of calls the browser
173174
* executes are never cut; an event
174175
* still too large is refused by the buffer, which ends the turn with an error.
@@ -190,8 +191,12 @@ export function compactStreamEvent(event: StreamEvent): StreamEvent {
190191
if (Buffer.byteLength(JSON.stringify(compacted)) > STREAM_EVENT_COMPACTION_THRESHOLD_BYTES) {
191192
compacted = trimArrays(compacted, skipKeys)
192193
}
193-
if (Buffer.byteLength(JSON.stringify(compacted)) > STREAM_EVENT_MAX_PAYLOAD_BYTES) {
194-
compacted = omitDominantBulk(toRecordOrNull(compacted) ?? {}, skipKeys)
194+
// Each pass replaces at least OMITTABLE_FIELD_MIN_BYTES, and stops when nothing is left to omit.
195+
while (Buffer.byteLength(JSON.stringify(compacted)) > STREAM_EVENT_MAX_PAYLOAD_BYTES) {
196+
const record = toRecordOrNull(compacted) ?? {}
197+
const omitted = omitDominantBulk(record, skipKeys)
198+
if (omitted === record) break
199+
compacted = omitted
195200
}
196201
return compacted === payload ? event : ({ ...event, payload: compacted } as StreamEvent)
197202
}

0 commit comments

Comments
 (0)