authorgravatar for 116843062+Kiri110K@users.noreply.github.comKiri110K <116843062+Kiri110K@users.noreply.github.com> 2026-05-09 05:00:24+09:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 13:00:24-07:00
log271d65e047c557b1bf74c66dc18c8f09d42893a0
tree7d684486d825e9d3e6d2c03323260197c2d197fa
parentaa8b9f2286fd8c4fcc36a240e34e8e89a3d793fa
signature Signed by PGP key B5690EEEBB952194

Fix OpenCode raw text delta assembly (#2526)


2 files changed, 87 insertions(+), 17 deletions(-)

apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+85-4
......@@ -559,17 +559,98 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
559559 }),
560560 );
561561
562 it.effect("deduplicates overlapping assistant text deltas after part updates", () =>
562 it.effect("appends raw assistant text deltas and reconciles part update snapshots", () =>
563563 Effect.sync(() => {
564564 const firstUpdate = mergeOpenCodeAssistantText(undefined, "Hello");
565565 const overlapDelta = appendOpenCodeAssistantTextDelta(firstUpdate.latestText, "lo world");
566 const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hello world!");
566 const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hellolo world");
567567
568568 assert.deepEqual(
569569 [firstUpdate.deltaToEmit, overlapDelta.deltaToEmit, secondUpdate.deltaToEmit],
570 ["Hello", " world", "!"],
570 ["Hello", "lo world", ""],
571571 );
572 assert.equal(secondUpdate.latestText, "Hello world!");
572 assert.equal(secondUpdate.latestText, "Hellolo world");
573 }),
574 );
575
576 it.effect("does not strip coincidental prefix overlap from OpenCode part deltas", () =>
577 Effect.gen(function* () {
578 const adapter = yield* OpenCodeAdapter;
579 const threadId = asThreadId("thread-opencode-raw-delta");
580 const part = {
581 id: "part-raw-delta",
582 sessionID: "http://127.0.0.1:9999/session",
583 messageID: "msg-raw-delta",
584 type: "text",
585 text: "A B",
586 time: { start: 1 },
587 };
588 runtimeMock.state.subscribedEvents = [
589 {
590 type: "message.updated",
591 properties: {
592 sessionID: "http://127.0.0.1:9999/session",
593 info: {
594 id: "msg-raw-delta",
595 role: "assistant",
596 },
597 },
598 },
599 {
600 type: "message.part.updated",
601 properties: {
602 sessionID: "http://127.0.0.1:9999/session",
603 part,
604 time: 1,
605 },
606 },
607 {
608 type: "message.part.delta",
609 properties: {
610 sessionID: "http://127.0.0.1:9999/session",
611 messageID: "msg-raw-delta",
612 partID: "part-raw-delta",
613 field: "text",
614 delta: "Bonus",
615 },
616 },
617 {
618 type: "message.part.updated",
619 properties: {
620 sessionID: "http://127.0.0.1:9999/session",
621 part: {
622 ...part,
623 text: "A BBonus",
624 time: { start: 1, end: 2 },
625 },
626 time: 2,
627 },
628 },
629 ];
630 const eventsFiber = yield* adapter.streamEvents.pipe(
631 Stream.filter((event) => event.threadId === threadId),
632 Stream.take(5),
633 Stream.runCollect,
634 Effect.forkChild,
635 );
636
637 yield* adapter.startSession({
638 provider: ProviderDriverKind.make("opencode"),
639 threadId,
640 runtimeMode: "full-access",
641 });
642
643 const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second")));
644 const deltas = events.filter((event) => event.type === "content.delta");
645 assert.deepEqual(
646 deltas.map((event) => (event.type === "content.delta" ? event.payload.delta : "")),
647 ["A B", "Bonus"],
648 );
649 assert.equal(events.at(-1)?.type, "item.completed");
650 const completed = events.at(-1);
651 if (completed?.type === "item.completed") {
652 assert.equal(completed.payload.detail, "A BBonus");
653 }
573654 }),
574655 );
575656
apps/server/src/provider/Layers/OpenCodeAdapter.ts+2-13
......@@ -311,16 +311,6 @@ function commonPrefixLength(left: string, right: string): number {
311311 return index;
312312}
313313
314function suffixPrefixOverlap(text: string, delta: string): number {
315 const maxLength = Math.min(text.length, delta.length);
316 for (let length = maxLength; length > 0; length -= 1) {
317 if (text.endsWith(delta.slice(0, length))) {
318 return length;
319 }
320 }
321 return 0;
322}
323
324314function resolveLatestAssistantText(previousText: string | undefined, nextText: string): string {
325315 if (previousText && previousText.length > nextText.length && previousText.startsWith(nextText)) {
326316 return previousText;
......@@ -349,10 +339,9 @@ export function appendOpenCodeAssistantTextDelta(
349339 readonly nextText: string;
350340 readonly deltaToEmit: string;
351341} {
352 const deltaToEmit = delta.slice(suffixPrefixOverlap(previousText, delta));
353342 return {
354 nextText: previousText + deltaToEmit,
355 deltaToEmit,
343 nextText: previousText + delta,
344 deltaToEmit: delta,
356345 };
357346}
358347