@clawchatsai/connector 0.0.91 → 0.0.92

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.91",
3
+ "version": "0.0.92",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
@@ -33,9 +33,7 @@ export class MessageController {
33
33
  const metadata = body.metadata ? JSON.stringify(body.metadata) : null;
34
34
  const existing = db.prepare('SELECT id, status, metadata FROM messages WHERE id = ?').get(body.id);
35
35
  if (existing) {
36
- if (body.status && body.status !== existing.status) {
37
- db.prepare('UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?').run(body.status, body.content, metadata || existing.metadata, body.id);
38
- }
36
+ db.prepare('UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?').run(body.status || existing.status, body.content, metadata || existing.metadata, body.id);
39
37
  } else {
40
38
  db.prepare('INSERT INTO messages (id, thread_id, role, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(body.id, params.id, body.role, body.content, body.status || 'sent', metadata, body.seq || null, body.timestamp, Date.now());
41
39
  db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(Date.now(), params.id);
package/server/gateway.js CHANGED
@@ -121,9 +121,9 @@ export class GatewayClient {
121
121
  if (!db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId)) { console.log(`Ignoring response for deleted thread: ${parsed.threadId}`); return; }
122
122
 
123
123
  let content = sanitizeAssistantContent(extractContent(message));
124
- if (!content?.trim()) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
125
124
 
126
- // Attach media (MEDIA: lines from exec stdout captured by after_tool_call hook)
125
+ // Attach media (MEDIA: lines from exec stdout captured by after_tool_call hook).
126
+ // Stash is read before the empty-content guard — media-only responses (no text) must not be dropped.
127
127
  const pendingPaths = this.mediaStash?.get(sessionKey) ?? [];
128
128
  this.mediaStash?.delete(sessionKey);
129
129
  const IMAGE_EXTS = new Set(['png','jpg','jpeg','gif','webp','bmp','svg','ico','avif','tiff']);
@@ -134,9 +134,12 @@ export class GatewayClient {
134
134
  if (IMAGE_EXTS.has(ext)) imagePaths.push(p);
135
135
  else pendingAttachments.push({ path: p, name: p.split('/').pop(), type: AUDIO_EXTS.has(ext) ? 'audio' : 'file' });
136
136
  }
137
- if (imagePaths.length > 0) content = content.trimEnd() + '\n\n' + imagePaths.map(p => `![image](${p})`).join('\n');
137
+ if (imagePaths.length > 0) content = (content?.trimEnd() || '') + '\n\n' + imagePaths.map(p => `![image](${p})`).join('\n');
138
138
  if (pendingPaths.length > 0) console.log(`[clawchats] media-attach: ${imagePaths.length} image(s), ${pendingAttachments.length} attachment(s) for ${sessionKey}`);
139
139
 
140
+ // Skip only if there is truly nothing to save — no text and no pending media.
141
+ if (!content?.trim() && pendingPaths.length === 0) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
142
+
140
143
  const now = Date.now();
141
144
  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);
142
145
  let messageId;
@@ -228,9 +231,15 @@ export class GatewayClient {
228
231
  if (stream === 'assistant') {
229
232
  const text = data?.text || '';
230
233
  if (text) {
234
+ const offset = log._assistantTextOffset || 0;
231
235
  let seg = log._currentAssistantSegment;
232
- if (!seg || seg._sealed) { seg = { type: 'assistant', timestamp: Date.now(), text, _sealed: false }; log._currentAssistantSegment = seg; log.steps.push(seg); }
233
- else seg.text = text;
236
+ if (!seg || seg._sealed) {
237
+ seg = { type: 'assistant', timestamp: Date.now(), text: text.substring(offset), _sealed: false };
238
+ log._currentAssistantSegment = seg;
239
+ log.steps.push(seg);
240
+ } else {
241
+ seg.text = text.substring(offset);
242
+ }
234
243
  }
235
244
  return;
236
245
  }
@@ -243,7 +252,11 @@ export class GatewayClient {
243
252
  if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) { log._lastThinkingBroadcast = now; this._broadcastActivityUpdate(runId, log); }
244
253
  }
245
254
  if (stream === 'tool') {
246
- if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
255
+ if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
256
+ const seg = log._currentAssistantSegment;
257
+ seg._sealed = true;
258
+ log._assistantTextOffset = (log._assistantTextOffset || 0) + seg.text.length;
259
+ }
247
260
  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') || '') : '';
248
261
  const step = { type: 'tool', timestamp: Date.now(), name: data?.name || 'unknown', phase: data?.phase || 'start', toolCallId: data?.toolCallId, meta: data?.meta || (argsMeta ? String(argsMeta) : undefined), isError: data?.isError || false };
249
262
  if (data?.phase === 'result') {
@@ -258,10 +271,19 @@ export class GatewayClient {
258
271
  this._broadcastActivityUpdate(runId, log);
259
272
  }
260
273
  if (stream === 'lifecycle' && (data?.phase === 'end' || data?.phase === 'error')) {
261
- if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
274
+ if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
275
+ const seg = log._currentAssistantSegment;
276
+ seg._sealed = true;
277
+ log._assistantTextOffset = (log._assistantTextOffset || 0) + seg.text.length;
278
+ }
262
279
  const idx = log.steps.findLastIndex(s => s.type === 'assistant');
263
280
  if (idx >= 0) log.steps.splice(idx, 1);
264
281
  writeActivityToDb(this.getDb, this.broadcastToBrowsers.bind(this), runId, log);
282
+ // Broadcast final state so browser can clean up any in-flight timers (e.g. aborted runs)
283
+ if (log._parsed && log._messageId) {
284
+ const cleanSteps = log.steps.map(s => { const c = { ...s }; delete c._sealed; return c; });
285
+ this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'activity-updated', workspace: log._parsed.workspace, threadId: log._parsed.threadId, messageId: log._messageId, activityLog: cleanSteps, activitySummary: generateActivitySummary(log.steps), final: true }));
286
+ }
265
287
  this.activityLogs.delete(runId);
266
288
  }
267
289
  }
package/server/index.js CHANGED
@@ -81,7 +81,7 @@ export function createApp(config = {}) {
81
81
  const { getWorkspaces, setWorkspaces } = createWorkspaceStore(WORKSPACES_FILE);
82
82
 
83
83
  const debugLogger = new DebugLogger(DATA_DIR);
84
- const mediaStash = new Map();
84
+ const mediaStash = config.mediaStash ?? new Map();
85
85
 
86
86
  const memoryConfig = discoverMemoryConfig(config.memoryEnv || {});
87
87
  const memoryProvider = createMemoryProvider(memoryConfig);