Skip to content

Commit daa035c

Browse files
StiensWoutclaude
andcommitted
fix(server): one ACP text flush publishes one text snapshot
A delayed flush could read segment text twice while chunks kept appending, so message.updated and turn_item.updated in the same emission could carry different text. Capture the text once, in the step that clears dirty. Clarify that the 50 ms flush window is scheduled, not a hard deadline. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 7362c0c commit daa035c

1 file changed

Lines changed: 13 additions & 8 deletions

File tree

‎packages/provider-acp/src/server/adapter.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,11 @@ export const ACP_PROTOCOL = "acp.ndjson-jsonrpc" as const;
131131
const ACP_DEFERRED_FINALIZE_DEBOUNCE: Duration.Input = "3000 millis";
132132

133133
/**
134-
* Latest streamed text reaches the event stream at most this long after its
135-
* chunk. A text segment's first chunk and its close emit at once; the chunks
136-
* between collapse into one full-text snapshot per window instead of one per
137-
* chunk, which grew with the square of the reply length.
134+
* Coalescing window for streamed text. A text segment's first chunk and its
135+
* close emit at once; a later chunk schedules one full-text snapshot this long
136+
* after it, and the chunks inside the window share it instead of each emitting
137+
* the whole reply, which grew with the square of the reply length. The flush
138+
* is scheduled, not a hard deadline, so it can land later under host load.
138139
*/
139140
const ACP_TEXT_FLUSH_INTERVAL: Duration.Input = "50 millis";
140141

@@ -2444,6 +2445,10 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
24442445
const emitsNode = completed || !segment.emitted;
24452446
segment.emitted = true;
24462447
segment.dirty = false;
2448+
// Read the text in the same step that clears `dirty`. Chunks keep
2449+
// appending while this emission yields; each one marks the segment
2450+
// dirty again and reaches the next flush or the close.
2451+
const text = segment.text;
24472452
const now = yield* DateTime.now;
24482453
const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId);
24492454
const nodeId = providerNodeId(segment.nativeItemId);
@@ -2487,7 +2492,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
24872492
runId: context.input.runId,
24882493
nodeId: messageNodeId,
24892494
role: kind,
2490-
text: segment.text,
2495+
text,
24912496
attachments: [],
24922497
streaming: !completed,
24932498
createdAt: segment.startedAt,
@@ -2519,7 +2524,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
25192524
type: "user_message",
25202525
messageId,
25212526
inputIntent: "turn_start",
2522-
text: segment.text,
2527+
text,
25232528
attachments: [],
25242529
},
25252530
}
@@ -2543,7 +2548,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
25432548
updatedAt: now,
25442549
type: "assistant_message",
25452550
messageId,
2546-
text: segment.text,
2551+
text,
25472552
streaming: !completed,
25482553
},
25492554
},
@@ -2570,7 +2575,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
25702575
completedAt: completed ? now : null,
25712576
updatedAt: now,
25722577
type: "reasoning",
2573-
text: segment.text,
2578+
text,
25742579
streaming: !completed,
25752580
},
25762581
});

0 commit comments

Comments
 (0)