synabun 2026.4.20-beta.8 → 2026.4.20-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,6 +99,7 @@ let _primaryAgents = new Set();
99
99
  let _sessions = [];
100
100
  const _userMsgIds = new Set(); // track user messageIDs to filter echoed SSE events
101
101
  const _partTypes = new Map(); // track partID → type (reasoning/text) from part.updated events
102
+ const _renderedAssistantMsgIds = new Set(); // prevents double-render when message.updated re-fires after finalize (Windows timing)
102
103
  let _activeSendRequestId = null; // WS request id of the in-flight message:send (for abort cancellation)
103
104
  let _renderSeq = 0; // guard against stale async renders overwriting fresh ones
104
105
  let _activeQuestionToolId = null; // toolId of the currently active (unanswered) question card
@@ -1163,6 +1164,15 @@ export function handleSSEEvent(eventType, event) {
1163
1164
  if (mid) _userMsgIds.add(mid);
1164
1165
  break;
1165
1166
  }
1167
+ // Guard: once we've rendered this assistant message's final payload, ignore
1168
+ // re-emitted message.updated events (OpenCode on Windows sometimes fires a
1169
+ // late duplicate after finalize, which would otherwise re-stream the whole
1170
+ // response into a brand-new assistant bubble).
1171
+ const assistantMid = info.id || event.messageID;
1172
+ if (assistantMid && _renderedAssistantMsgIds.has(assistantMid)) {
1173
+ update();
1174
+ break;
1175
+ }
1166
1176
  // If no streaming element was built by prior deltas, render the
1167
1177
  // assistant's content directly from the event (non-streaming path)
1168
1178
  const hasStreaming = container.querySelector('.ocp-msg-assistant.streaming');
@@ -1176,6 +1186,9 @@ export function handleSSEEvent(eventType, event) {
1176
1186
  removeThinking(container);
1177
1187
  finalizeStreamingMessage(container);
1178
1188
  _partTypes.clear();
1189
+ if (assistantMid && (explicitTerminal || info.tokens || info.usage)) {
1190
+ _renderedAssistantMsgIds.add(assistantMid);
1191
+ }
1179
1192
  if (info.tokens || info.usage) {
1180
1193
  const raw = info.tokens || info.usage || {};
1181
1194
  tab.threadTokenUsage = normalizeThreadTokenUsage(raw);
@@ -1287,6 +1300,14 @@ export function handleSSEEvent(eventType, event) {
1287
1300
  }
1288
1301
  case 'message.completed': {
1289
1302
  removeThinking(container);
1303
+ const completedMid = event.messageID || event.id;
1304
+ if (completedMid && _renderedAssistantMsgIds.has(completedMid)) {
1305
+ tab.running = false;
1306
+ tab.turnStartedAt = 0;
1307
+ setTurnStatus(tab);
1308
+ update();
1309
+ break;
1310
+ }
1290
1311
  // Fallback: render content if no streaming element was built
1291
1312
  const hasStreamingLegacy = container.querySelector('.ocp-msg-assistant.streaming');
1292
1313
  renderAssistantPayload(container, event.content ?? event.parts ?? event.text ?? '', {
@@ -1294,6 +1315,7 @@ export function handleSSEEvent(eventType, event) {
1294
1315
  });
1295
1316
  finalizeStreamingMessage(container);
1296
1317
  _partTypes.clear();
1318
+ if (completedMid) _renderedAssistantMsgIds.add(completedMid);
1297
1319
  tab.running = false;
1298
1320
  tab.turnStartedAt = 0;
1299
1321
  setTurnStatus(tab);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.20-beta.8",
3
+ "version": "2026.4.20-beta.9",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",