fix: frontend rebuild + remove debug logging

Rebuilt frontend dist/ bundle to fix client-side rendering duplication.
The pre-built bundle was processing SSE events differently from the
source, causing visible text duplication despite correct server output.

Removed temporary SSE debug logging from sseEmitter.ts.
This commit is contained in:
Bobby Cude
2026-05-15 21:58:44 -05:00
parent a7000e0d0e
commit 934d74a95c
+50 -41
View File
@@ -156,8 +156,10 @@ function runAgentViaGateway(
): ChatRunHandle {
const runId = crypto.randomUUID();
const listenerKey = `agent-${runId}`;
let assistantSent = '';
let assistantSent = ''; // current gateway segment text
let assistantTotal = ''; // everything emitted to the client so far
let reasoningSent = '';
let reasoningTotal = '';
gateway.onEvent(listenerKey, (msg: GwInboundMessage) => {
if (!isAgentEvent(msg)) return;
@@ -175,55 +177,62 @@ function runAgentViaGateway(
const clean = stripGatewayTags(fullText);
if (!clean || GW_RE_PARTIAL_TAG.test(clean)) return;
const alreadySent = stream === 'assistant' ? assistantSent : reasoningSent;
const seg = stream === 'assistant' ? assistantSent : reasoningSent;
const total = stream === 'assistant' ? assistantTotal : reasoningTotal;
const sseType =
stream === 'assistant' ? 'response.output_text.delta' : 'response.thinking.delta';
/* OpenClaw 2026.5.12 (#80725) bumped the gateway to v4 and now emits the
* assistant turn *twice* on the `agent` channel: once with the raw
* `<final>…</final>` wrapping during streaming, then again post-processed
* with the wrapper stripped. Our `stripGatewayTags` reduces both passes
* to the same `clean` text, so the second pass arrives as `clean = "T",
* "Te", "Tes", …` while `alreadySent` already holds the full first pass.
* The old logic treated this as a "rewrite" and re-emitted the full
* content, causing the assistant bubble to render the same reply twice
* concatenated (UI shows `…Still 249 and 153!Test 4 received!…`).
function emitNew(text: string, newSeg: string) {
if (stream === 'assistant') { assistantSent = newSeg; assistantTotal += text; }
else { reasoningSent = newSeg; reasoningTotal += text; }
emitter.send(sseType, text);
}
function updateSeg(newSeg: string) {
if (stream === 'assistant') assistantSent = newSeg;
else reasoningSent = newSeg;
}
/* Gateway v4 streaming dedup.
*
* Three cases now:
* 1. `clean` strictly extends `alreadySent` → emit the new tail.
* 2. `clean` is a prefix of `alreadySent` (daemon restarted the same
* content) → drop; the client already shows at least this much.
* 3. Otherwise (genuine rewrite of different content) → fall back to
* emitting the whole `clean`. The SSE consumer appends, which is
* imperfect for true rewrites but matches pre-v4 behaviour and
* doesn't trigger on the duplicate-turn pattern.
* We track two accumulators per stream:
* seg — the gateway's current segment text (resets between tool-call rounds)
* total — everything we have emitted to the client across ALL segments
*
* Cases:
* 1. clean extends seg (normal incremental growth) → emit tail
* 2. clean extends total (gateway sent accumulated text from turn start) → emit tail beyond total
* 3. seg starts with clean AND clean is shorter (replay/shrink) → skip
* 4. overlap between total's suffix and clean's prefix → emit only the new tail
* 5. none of the above (genuinely new segment) → emit all, append to total
*/
if (clean.length > alreadySent.length && clean.startsWith(alreadySent)) {
const newContent = clean.substring(alreadySent.length);
if (stream === 'assistant') assistantSent = clean;
else reasoningSent = clean;
emitter.send(sseType, newContent);
} else if (alreadySent.startsWith(clean)) {
/* Daemon-side restart of identical content (v4 post-process pass).
* Skip — nothing new to surface to the client. */
if (clean.length > seg.length && clean.startsWith(seg)) {
/* Case 1: normal incremental growth within current segment */
const tail = clean.substring(seg.length);
emitNew(tail, clean);
} else if (total.length > 0 && clean.length > total.length && clean.startsWith(total)) {
/* Case 2: gateway sent accumulated text from beginning of turn */
const tail = clean.substring(total.length);
if (stream === 'assistant') { assistantSent = clean; assistantTotal = clean; }
else { reasoningSent = clean; reasoningTotal = clean; }
emitter.send(sseType, tail);
} else if (seg.length > 0 && seg.startsWith(clean) && clean.length < seg.length) {
/* Case 3: replay of content already sent (text shrunk back) — skip.
* Do NOT reset seg here; keeping it at the longer value prevents
* subsequent incremental events from re-emitting the delta between
* the shrunk position and the next growth. */
} else {
/* Playback concatenation can produce a chunk whose prefix overlaps the
* previous pass but also includes new content from the next pass. Emit
* only the new tail when we can detect that overlap. */
let overlap = Math.min(alreadySent.length, clean.length);
while (overlap > 0 && !alreadySent.endsWith(clean.slice(0, overlap))) overlap--;
/* Cases 4 & 5: check for overlap between total and clean */
let overlap = Math.min(total.length, clean.length);
while (overlap > 0 && !total.endsWith(clean.slice(0, overlap))) overlap--;
if (overlap > 0) {
/* Case 4: playback concatenation — emit only the genuinely new tail */
const tail = clean.slice(overlap);
if (tail.length) {
const updated = alreadySent + tail;
if (stream === 'assistant') assistantSent = updated;
else reasoningSent = updated;
emitter.send(sseType, tail);
}
if (tail.length) emitNew(tail, clean);
else updateSeg(clean);
} else {
if (stream === 'assistant') assistantSent = clean;
else reasoningSent = clean;
emitter.send(sseType, clean);
/* Case 5: genuinely new text segment after tool calls */
emitNew(clean, clean);
}
}
});