@clawchatsai/connector 0.0.94 → 0.0.95
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.
- package/package.json +1 -1
- package/server/gateway.js +42 -33
package/package.json
CHANGED
package/server/gateway.js
CHANGED
|
@@ -106,6 +106,7 @@ export class GatewayClient {
|
|
|
106
106
|
const parsed = parseSessionKey(sessionKey);
|
|
107
107
|
if (parsed) {
|
|
108
108
|
const existing = this.streamState.get(sessionKey) || { buffer: '', threadId: parsed.threadId, state: 'streaming', held: [] };
|
|
109
|
+
const prevLen = existing.buffer.length; // capture before update — used to advance thoughtStartOffset on first post-tool delta
|
|
109
110
|
existing.buffer = extractContent(message); // gateway sends full cumulative content per delta, not chunks
|
|
110
111
|
if (isSilentReplyPrefix(existing.buffer, 'NO_REPLY') || isSilentReplyPrefix(existing.buffer, 'HEARTBEAT_OK')) {
|
|
111
112
|
existing.held = existing.held || [];
|
|
@@ -119,8 +120,16 @@ export class GatewayClient {
|
|
|
119
120
|
}
|
|
120
121
|
this.streamState.set(sessionKey, existing);
|
|
121
122
|
this.broadcastToBrowsers(rawData); // dual-emit: raw chat event (old protocol)
|
|
122
|
-
//
|
|
123
|
-
//
|
|
123
|
+
// On the first delta after a tool result, advance the segment offset and signal the
|
|
124
|
+
// frontend to clear the bubble. Both happen here — right before the new-segment delta —
|
|
125
|
+
// so the clear and fill are atomic from the browser's perspective.
|
|
126
|
+
if (existing.pendingReset) {
|
|
127
|
+
existing.thoughtStartOffset = prevLen;
|
|
128
|
+
existing.pendingReset = false;
|
|
129
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'streaming-reset', threadId: parsed.threadId, workspace: parsed.workspace }));
|
|
130
|
+
}
|
|
131
|
+
// Broadcast only the current-segment portion (text after the last tool call offset).
|
|
132
|
+
// thoughtStartOffset advances on first post-tool delta; 0 means no tools yet → full buffer.
|
|
124
133
|
const visibleContent = existing.buffer.substring(existing.thoughtStartOffset || 0);
|
|
125
134
|
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'streaming-delta', threadId: parsed.threadId, workspace: parsed.workspace, content: visibleContent }));
|
|
126
135
|
} else {
|
|
@@ -215,11 +224,14 @@ export class GatewayClient {
|
|
|
215
224
|
if (imagePaths.length > 0) content = (content?.trimEnd() || '') + '\n\n' + imagePaths.map(p => ``).join('\n');
|
|
216
225
|
if (pendingPaths.length > 0) console.log(`[clawchats] media-attach: ${imagePaths.length} image(s), ${pendingAttachments.length} attachment(s) for ${sessionKey}`);
|
|
217
226
|
|
|
218
|
-
// Skip only if there is truly nothing to save — no text and no pending media.
|
|
219
|
-
if (!content?.trim() && pendingPaths.length === 0) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
220
|
-
|
|
221
227
|
const now = Date.now();
|
|
222
228
|
const pendingMsg = db.prepare(`SELECT id, metadata FROM messages WHERE thread_id = ? AND role = 'assistant' AND json_extract(metadata, '$.pending') = 1 ORDER BY timestamp DESC LIMIT 1`).get(parsed.threadId);
|
|
229
|
+
|
|
230
|
+
// Skip only if there is truly nothing to save AND no pending row to resolve.
|
|
231
|
+
// If a pending row exists, always proceed to update it (even with empty content) so the
|
|
232
|
+
// pending flag is cleared. Without this, tool-only responses (no post-tool text) leave
|
|
233
|
+
// pending=true forever, and the startup cleanup marks them '[Response interrupted]'.
|
|
234
|
+
if (!content?.trim() && pendingPaths.length === 0 && !pendingMsg) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
223
235
|
let messageId;
|
|
224
236
|
|
|
225
237
|
if (pendingMsg) {
|
|
@@ -308,25 +320,6 @@ export class GatewayClient {
|
|
|
308
320
|
if (!this.activityLogs.has(runId)) this.activityLogs.set(runId, { sessionKey, steps: [], startTime: Date.now() });
|
|
309
321
|
const log = this.activityLogs.get(runId);
|
|
310
322
|
|
|
311
|
-
if (stream === 'assistant') {
|
|
312
|
-
// Derive narration text from streamState.buffer (same source as thoughtStartOffset).
|
|
313
|
-
// This eliminates the _assistantTextOffset race: no longer depends on cumulative
|
|
314
|
-
// stream:assistant text which can arrive out of order relative to stream:tool start.
|
|
315
|
-
const streamEntry = this.streamState.get(sessionKey);
|
|
316
|
-
if (!streamEntry) return;
|
|
317
|
-
const narrationStart = log._lastNarrationStart ?? 0;
|
|
318
|
-
const narrationText = streamEntry.buffer.substring(narrationStart);
|
|
319
|
-
if (!narrationText) return;
|
|
320
|
-
let seg = log._currentAssistantSegment;
|
|
321
|
-
if (!seg || seg._sealed) {
|
|
322
|
-
seg = { type: 'assistant', timestamp: Date.now(), text: narrationText, _sealed: false };
|
|
323
|
-
log._currentAssistantSegment = seg;
|
|
324
|
-
log.steps.push(seg);
|
|
325
|
-
} else {
|
|
326
|
-
seg.text = narrationText;
|
|
327
|
-
}
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
323
|
if (stream === 'thinking') {
|
|
331
324
|
let step = log.steps.find(s => s.type === 'thinking');
|
|
332
325
|
if (step) step.text = data?.text || '';
|
|
@@ -336,7 +329,22 @@ export class GatewayClient {
|
|
|
336
329
|
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) { log._lastThinkingBroadcast = now; this._broadcastActivityUpdate(runId, log); }
|
|
337
330
|
}
|
|
338
331
|
if (stream === 'tool') {
|
|
339
|
-
|
|
332
|
+
// At phase:start the gateway has already flushed the text buffer, so the buffer is
|
|
333
|
+
// complete. Capture the full inter-tool narration now — no race, no stale snapshots.
|
|
334
|
+
// For result/update phases, just defensively seal any still-open segment.
|
|
335
|
+
if (data?.phase !== 'result' && data?.phase !== 'update') {
|
|
336
|
+
const streamEntry = this.streamState.get(sessionKey);
|
|
337
|
+
const narrationStart = log._lastNarrationStart ?? 0;
|
|
338
|
+
const narrationText = streamEntry ? streamEntry.buffer.substring(narrationStart) : '';
|
|
339
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
340
|
+
if (narrationText.trim()) log._currentAssistantSegment.text = narrationText;
|
|
341
|
+
log._currentAssistantSegment._sealed = true;
|
|
342
|
+
} else if (narrationText.trim()) {
|
|
343
|
+
const seg = { type: 'assistant', timestamp: Date.now(), text: narrationText, _sealed: true };
|
|
344
|
+
log._currentAssistantSegment = seg;
|
|
345
|
+
log.steps.push(seg);
|
|
346
|
+
}
|
|
347
|
+
} else if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
340
348
|
log._currentAssistantSegment._sealed = true;
|
|
341
349
|
}
|
|
342
350
|
const argsMeta = data?.args ? (data.args.command || data.args.path || data.args.query || data.args.url || Object.values(data.args).find(v => typeof v === 'string') || '') : '';
|
|
@@ -350,12 +358,15 @@ export class GatewayClient {
|
|
|
350
358
|
// so the response area always shows only the most recent thought / final answer.
|
|
351
359
|
const streamEntry = this.streamState.get(sessionKey);
|
|
352
360
|
if (streamEntry && log._parsed) {
|
|
353
|
-
|
|
361
|
+
// Don't advance thoughtStartOffset or broadcast streaming-reset yet.
|
|
362
|
+
// Wait for the first post-tool delta before doing either:
|
|
363
|
+
// (a) No post-tool text: thoughtStartOffset stays at the current segment start,
|
|
364
|
+
// bubble retains the last segment text as the final answer.
|
|
365
|
+
// (b) Post-tool text arrives: delta handler advances the offset and broadcasts
|
|
366
|
+
// streaming-reset immediately before the first new-segment delta, so the
|
|
367
|
+
// bubble clears at exactly the right moment.
|
|
368
|
+
streamEntry.pendingReset = true;
|
|
354
369
|
log._lastNarrationStart = streamEntry.buffer.length; // align activity log segments with buffer
|
|
355
|
-
// Signal the frontend to clear the response area — new thought is starting.
|
|
356
|
-
// streaming-reset fires before activity-updated so the typing-indicator is visible
|
|
357
|
-
// when startIndicatorTimer attaches to it in the allToolsDone handler.
|
|
358
|
-
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'streaming-reset', threadId: log._parsed.threadId, workspace: log._parsed.workspace }));
|
|
359
370
|
}
|
|
360
371
|
} else if (data?.phase === 'update') {
|
|
361
372
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId);
|
|
@@ -368,8 +379,6 @@ export class GatewayClient {
|
|
|
368
379
|
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
369
380
|
log._currentAssistantSegment._sealed = true;
|
|
370
381
|
}
|
|
371
|
-
const idx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
372
|
-
if (idx >= 0) log.steps.splice(idx, 1);
|
|
373
382
|
writeActivityToDb(this.getDb, this.broadcastToBrowsers.bind(this), runId, log);
|
|
374
383
|
// Store finalized state — streaming-end (handleChatEvent) will pick it up and carry it
|
|
375
384
|
// as one atomic payload. Do NOT broadcast activity-updated here anymore.
|
|
@@ -431,7 +440,7 @@ export class GatewayClient {
|
|
|
431
440
|
if (state.state === 'streaming' && !(state.held?.length > 0)) {
|
|
432
441
|
const parsed = parseSessionKey(sessionKey);
|
|
433
442
|
// Include both old shape (sessionKey/buffer) and new shape (workspace/content) for dual-emit compat
|
|
434
|
-
streams.push({ sessionKey, threadId: state.threadId, buffer: state.buffer, ...(parsed ? { workspace: parsed.workspace, content: state.buffer } : {}) });
|
|
443
|
+
streams.push({ sessionKey, threadId: state.threadId, buffer: state.buffer, ...(parsed ? { workspace: parsed.workspace, content: state.buffer.substring(state.thoughtStartOffset || 0) } : {}) });
|
|
435
444
|
}
|
|
436
445
|
}
|
|
437
446
|
if (streams.length > 0) ws.send(JSON.stringify({ type: 'clawchats', event: 'stream-sync', streams }));
|