summaryrefslogtreecommitdiffhomepage
path: root/src/core/chunks/trim.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-22 14:18:56 +0900
committerAdam Malczewski <[email protected]>2026-06-22 14:18:56 +0900
commitb3c830237206a319348a0eb7078b49e20f60883b (patch)
tree2eab38e02b5e0ab209185a92e3ac60f54ea56415 /src/core/chunks/trim.ts
parent4e1d041c0ee34fa1b74b0d5ecbd432cdacf696e9 (diff)
downloaddispatch-web-b3c830237206a319348a0eb7078b49e20f60883b.tar.gz
dispatch-web-b3c830237206a319348a0eb7078b49e20f60883b.zip
fix: trim provisional chunks during long turns (browser stays responsive)
trimTranscript now drops oldest provisional chunks (the in-flight turn) when committed chunks are exhausted. Previously it bailed with drop=0 when committed was empty, allowing unbounded provisional growth during long generating turns (300+ chunks → browser crawls). Root cause of the syncTail approach failing: the kernel emits step-complete (line 360) BEFORE calling onStepComplete (line 542) — chunks are persisted only after tool results come back, not when step-complete fires. So syncTail on step-complete found nothing. Reverted the applyHistory + syncTail-on-step-complete changes from 4e1d041. The new approach is simpler: trim provisional directly in trimTranscript. Dropped chunks are lost temporarily (no Show Earlier) but come back as committed when the turn seals and syncTail fetches everything from the server. 686 tests green.
Diffstat (limited to 'src/core/chunks/trim.ts')
-rw-r--r--src/core/chunks/trim.ts32
1 files changed, 28 insertions, 4 deletions
diff --git a/src/core/chunks/trim.ts b/src/core/chunks/trim.ts
index 94065b3..7791721 100644
--- a/src/core/chunks/trim.ts
+++ b/src/core/chunks/trim.ts
@@ -84,7 +84,9 @@ function dropOldest(state: TranscriptState, drop: number): TranscriptState {
* quarters (`unloadCount(limit)` each) of the OLDEST committed chunks until back
* at/under the limit — normally exactly one quarter (limit 100: 101 → 76); more
* only when trimming was deferred (e.g. while the reader was scrolled up).
- * At/under the limit this is the identity. Never drops provisional chunks.
+ * At/under the limit this is the identity. When committed chunks are
+ * exhausted, also drops the oldest provisional chunks (the in-flight turn)
+ * to keep the browser responsive during very long turns.
*/
export function trimTranscript(state: TranscriptState, limit: number): TranscriptState {
if (!Number.isFinite(limit) || limit <= 0) return state;
@@ -92,9 +94,31 @@ export function trimTranscript(state: TranscriptState, limit: number): Transcrip
if (total <= limit) return state;
const quarter = unloadCount(limit);
const passes = Math.ceil((total - limit) / quarter);
- const drop = Math.min(passes * quarter, state.committed.length);
- if (drop <= 0) return state;
- return dropOldest(state, drop);
+
+ // First, drop oldest committed chunks (the usual path).
+ const committedDrop = Math.min(passes * quarter, state.committed.length);
+ let next = committedDrop > 0 ? dropOldest(state, committedDrop) : state;
+
+ // If still over the limit and committed is exhausted, drop oldest
+ // provisional chunks (the in-flight turn). These chunks have no seq
+ // (not yet persisted) and can't be "Show earlier" — but dropping them
+ // keeps the browser responsive. They'll come back as committed when
+ // the turn seals and syncTail fetches them from the server.
+ const remaining = totalCount(next);
+ if (remaining > limit && next.provisional.length > 0) {
+ const provisionalDrop = Math.min(
+ Math.ceil((remaining - limit) / quarter) * quarter,
+ next.provisional.length,
+ );
+ if (provisionalDrop > 0) {
+ next = {
+ ...next,
+ provisional: next.provisional.slice(provisionalDrop),
+ };
+ }
+ }
+
+ return next;
}
/**