@clawchatsai/connector 0.0.28 → 0.0.30
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.js +216 -284
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -832,12 +832,14 @@ function handleGetThreads(req, res, params, query) {
|
|
|
832
832
|
`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
|
|
833
833
|
).get(...matchingIds).c;
|
|
834
834
|
threads = db.prepare(
|
|
835
|
-
`SELECT
|
|
835
|
+
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
836
|
+
FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
836
837
|
).all(...matchingIds, limit, offset);
|
|
837
838
|
} else {
|
|
838
839
|
total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
|
|
839
840
|
threads = db.prepare(
|
|
840
|
-
|
|
841
|
+
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
842
|
+
FROM threads t ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
841
843
|
).all(limit, offset);
|
|
842
844
|
}
|
|
843
845
|
|
|
@@ -2228,9 +2230,19 @@ class GatewayClient {
|
|
|
2228
2230
|
|
|
2229
2231
|
// Clean up stale activity logs every 5 minutes (runs that never completed)
|
|
2230
2232
|
setInterval(() => {
|
|
2231
|
-
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
2233
|
+
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
2232
2234
|
for (const [runId, log] of this.activityLogs) {
|
|
2233
2235
|
if (log.startTime < cutoff) {
|
|
2236
|
+
if (log._messageId) {
|
|
2237
|
+
const db = getDb(log._parsed?.workspace);
|
|
2238
|
+
if (db) {
|
|
2239
|
+
db.prepare(`
|
|
2240
|
+
UPDATE messages SET content = '[Response interrupted]',
|
|
2241
|
+
metadata = json_remove(metadata, '$.pending')
|
|
2242
|
+
WHERE id = ? AND content = ''
|
|
2243
|
+
`).run(log._messageId);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2234
2246
|
this.activityLogs.delete(runId);
|
|
2235
2247
|
}
|
|
2236
2248
|
}
|
|
@@ -2311,17 +2323,6 @@ class GatewayClient {
|
|
|
2311
2323
|
this.broadcastGatewayStatus(true);
|
|
2312
2324
|
}
|
|
2313
2325
|
|
|
2314
|
-
// Handle activity log history responses
|
|
2315
|
-
if (msg.type === 'res' && msg.id && this._pendingActivityCallbacks?.has(msg.id)) {
|
|
2316
|
-
const callback = this._pendingActivityCallbacks.get(msg.id);
|
|
2317
|
-
this._pendingActivityCallbacks.delete(msg.id);
|
|
2318
|
-
if (msg.ok) {
|
|
2319
|
-
callback(msg.payload);
|
|
2320
|
-
} else {
|
|
2321
|
-
callback(null);
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
2326
|
// Forward all messages to browser clients
|
|
2326
2327
|
this.broadcastToBrowsers(data);
|
|
2327
2328
|
|
|
@@ -2369,9 +2370,8 @@ class GatewayClient {
|
|
|
2369
2370
|
|
|
2370
2371
|
saveAssistantMessage(sessionKey, message, seq) {
|
|
2371
2372
|
const parsed = parseSessionKey(sessionKey);
|
|
2372
|
-
if (!parsed) return;
|
|
2373
|
+
if (!parsed) return;
|
|
2373
2374
|
|
|
2374
|
-
// Guard: verify workspace still exists
|
|
2375
2375
|
const ws = getWorkspaces();
|
|
2376
2376
|
if (!ws.workspaces[parsed.workspace]) {
|
|
2377
2377
|
console.log(`Ignoring response for deleted workspace: ${parsed.workspace}`);
|
|
@@ -2380,49 +2380,66 @@ class GatewayClient {
|
|
|
2380
2380
|
|
|
2381
2381
|
const db = getDb(parsed.workspace);
|
|
2382
2382
|
|
|
2383
|
-
// Guard: verify thread still exists
|
|
2384
2383
|
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2385
2384
|
if (!thread) {
|
|
2386
2385
|
console.log(`Ignoring response for deleted thread: ${parsed.threadId}`);
|
|
2387
2386
|
return;
|
|
2388
2387
|
}
|
|
2389
2388
|
|
|
2390
|
-
// Extract content
|
|
2391
2389
|
const content = extractContent(message);
|
|
2392
|
-
|
|
2393
|
-
// Guard: skip empty content
|
|
2394
2390
|
if (!content || !content.trim()) {
|
|
2395
2391
|
console.log(`Skipping empty assistant response for thread ${parsed.threadId}`);
|
|
2396
2392
|
return;
|
|
2397
2393
|
}
|
|
2398
2394
|
|
|
2399
|
-
// Deterministic message ID from seq (deduplicates tool-call loops)
|
|
2400
2395
|
const now = Date.now();
|
|
2401
|
-
const messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
2402
2396
|
|
|
2403
|
-
//
|
|
2404
|
-
|
|
2397
|
+
// Check for pending activity message
|
|
2398
|
+
const pendingMsg = db.prepare(`
|
|
2399
|
+
SELECT id, metadata FROM messages
|
|
2400
|
+
WHERE thread_id = ? AND role = 'assistant'
|
|
2401
|
+
AND json_extract(metadata, '$.pending') = 1
|
|
2402
|
+
ORDER BY timestamp DESC LIMIT 1
|
|
2403
|
+
`).get(parsed.threadId);
|
|
2404
|
+
|
|
2405
|
+
let messageId;
|
|
2406
|
+
|
|
2407
|
+
if (pendingMsg) {
|
|
2408
|
+
// Merge final content into existing activity row
|
|
2409
|
+
const metadata = pendingMsg.metadata ? JSON.parse(pendingMsg.metadata) : {};
|
|
2410
|
+
delete metadata.pending;
|
|
2411
|
+
|
|
2412
|
+
// Clean up: remove last assistant narration (it's the final reply text)
|
|
2413
|
+
if (metadata.activityLog) {
|
|
2414
|
+
const lastAssistantIdx = metadata.activityLog.findLastIndex(s => s.type === 'assistant');
|
|
2415
|
+
if (lastAssistantIdx >= 0) metadata.activityLog.splice(lastAssistantIdx, 1);
|
|
2416
|
+
metadata.activitySummary = this.generateActivitySummary(metadata.activityLog);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
db.prepare('UPDATE messages SET content = ?, metadata = ?, timestamp = ? WHERE id = ?')
|
|
2420
|
+
.run(content, JSON.stringify(metadata), now, pendingMsg.id);
|
|
2421
|
+
|
|
2422
|
+
messageId = pendingMsg.id;
|
|
2423
|
+
} else {
|
|
2424
|
+
// No pending activity — normal INSERT (simple responses, no tools)
|
|
2425
|
+
messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
2405
2426
|
db.prepare(`
|
|
2406
2427
|
INSERT INTO messages (id, thread_id, role, content, status, timestamp, created_at)
|
|
2407
2428
|
VALUES (?, ?, 'assistant', ?, 'sent', ?, ?)
|
|
2408
2429
|
ON CONFLICT(id) DO UPDATE SET content = excluded.content, timestamp = excluded.timestamp
|
|
2409
2430
|
`).run(messageId, parsed.threadId, content, now, now);
|
|
2431
|
+
}
|
|
2410
2432
|
|
|
2411
|
-
|
|
2433
|
+
// Thread timestamp + unreads + broadcast (same for both paths)
|
|
2434
|
+
try {
|
|
2412
2435
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
2413
|
-
|
|
2414
|
-
// Always mark as unread server-side. The browser client is responsible
|
|
2415
|
-
// for sending read receipts (active-thread) to clear unreads — same
|
|
2416
|
-
// pattern as Slack/Discord. Server doesn't track viewport state.
|
|
2417
2436
|
db.prepare('INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)').run(parsed.threadId, messageId, now);
|
|
2418
2437
|
syncThreadUnreadCount(db, parsed.threadId);
|
|
2419
2438
|
|
|
2420
|
-
// Get thread title and unread info for notification
|
|
2421
2439
|
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2422
2440
|
const unreadCount = db.prepare('SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?').get(parsed.threadId).c;
|
|
2423
2441
|
const preview = content.length > 120 ? content.substring(0, 120) + '...' : content;
|
|
2424
2442
|
|
|
2425
|
-
// Broadcast message-saved for active thread reload
|
|
2426
2443
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2427
2444
|
type: 'clawchats',
|
|
2428
2445
|
event: 'message-saved',
|
|
@@ -2435,7 +2452,6 @@ class GatewayClient {
|
|
|
2435
2452
|
unreadCount
|
|
2436
2453
|
}));
|
|
2437
2454
|
|
|
2438
|
-
// Always broadcast unread-update — browser sends read receipts to clear
|
|
2439
2455
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
2440
2456
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2441
2457
|
type: 'clawchats',
|
|
@@ -2451,7 +2467,7 @@ class GatewayClient {
|
|
|
2451
2467
|
timestamp: now
|
|
2452
2468
|
}));
|
|
2453
2469
|
|
|
2454
|
-
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (seq:
|
|
2470
|
+
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (${pendingMsg ? 'merged into pending' : 'seq: ' + seq})`);
|
|
2455
2471
|
} catch (e) {
|
|
2456
2472
|
console.error(`Failed to save assistant message:`, e.message);
|
|
2457
2473
|
}
|
|
@@ -2498,8 +2514,6 @@ class GatewayClient {
|
|
|
2498
2514
|
// Capture intermediate text turns (narration between tool calls)
|
|
2499
2515
|
const text = data?.text || '';
|
|
2500
2516
|
if (text) {
|
|
2501
|
-
// Update or create the current assistant text segment
|
|
2502
|
-
// Each new assistant segment starts after a tool call
|
|
2503
2517
|
let currentSegment = log._currentAssistantSegment;
|
|
2504
2518
|
if (!currentSegment || currentSegment._sealed) {
|
|
2505
2519
|
currentSegment = {
|
|
@@ -2515,15 +2529,11 @@ class GatewayClient {
|
|
|
2515
2529
|
}
|
|
2516
2530
|
}
|
|
2517
2531
|
// Don't broadcast on every assistant delta — too noisy
|
|
2518
|
-
// We'll broadcast when a tool starts (which seals the current segment)
|
|
2519
2532
|
return;
|
|
2520
2533
|
}
|
|
2521
2534
|
|
|
2522
2535
|
if (stream === 'thinking') {
|
|
2523
|
-
// Reasoning/thinking text from the model (requires reasoningLevel: "stream")
|
|
2524
2536
|
const thinkingText = data?.text || '';
|
|
2525
|
-
const delta = data?.delta || '';
|
|
2526
|
-
// Update or create thinking step — we keep a single thinking step that accumulates
|
|
2527
2537
|
let thinkingStep = log.steps.find(s => s.type === 'thinking');
|
|
2528
2538
|
if (thinkingStep) {
|
|
2529
2539
|
thinkingStep.text = thinkingText;
|
|
@@ -2534,11 +2544,12 @@ class GatewayClient {
|
|
|
2534
2544
|
text: thinkingText
|
|
2535
2545
|
});
|
|
2536
2546
|
}
|
|
2537
|
-
//
|
|
2547
|
+
// Always write to DB; throttle broadcasts to every 300ms
|
|
2548
|
+
this._writeActivityToDb(runId, log);
|
|
2538
2549
|
const now = Date.now();
|
|
2539
2550
|
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) {
|
|
2540
2551
|
log._lastThinkingBroadcast = now;
|
|
2541
|
-
this.
|
|
2552
|
+
this._broadcastActivityUpdate(runId, log);
|
|
2542
2553
|
}
|
|
2543
2554
|
}
|
|
2544
2555
|
|
|
@@ -2546,8 +2557,6 @@ class GatewayClient {
|
|
|
2546
2557
|
// Seal any current assistant text segment (narration before this tool call)
|
|
2547
2558
|
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
2548
2559
|
log._currentAssistantSegment._sealed = true;
|
|
2549
|
-
// Broadcast the narration + upcoming tool
|
|
2550
|
-
this.broadcastActivityUpdate(runId, log);
|
|
2551
2560
|
}
|
|
2552
2561
|
|
|
2553
2562
|
const step = {
|
|
@@ -2573,21 +2582,18 @@ class GatewayClient {
|
|
|
2573
2582
|
log.steps.push(step);
|
|
2574
2583
|
}
|
|
2575
2584
|
} else if (data?.phase === 'update') {
|
|
2576
|
-
// Merge update events into the existing step — don't create new entries
|
|
2577
2585
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId);
|
|
2578
2586
|
if (existing) {
|
|
2579
|
-
// Update meta if the update carries new info
|
|
2580
2587
|
if (data?.meta) existing.resultMeta = data.meta;
|
|
2581
2588
|
if (data?.isError) existing.isError = true;
|
|
2582
2589
|
existing.phase = 'running';
|
|
2583
2590
|
}
|
|
2584
|
-
// If no existing step found, silently ignore the orphaned update
|
|
2585
2591
|
} else {
|
|
2586
2592
|
log.steps.push(step);
|
|
2587
2593
|
}
|
|
2588
2594
|
|
|
2589
|
-
|
|
2590
|
-
this.
|
|
2595
|
+
this._writeActivityToDb(runId, log);
|
|
2596
|
+
this._broadcastActivityUpdate(runId, log);
|
|
2591
2597
|
}
|
|
2592
2598
|
|
|
2593
2599
|
if (stream === 'lifecycle') {
|
|
@@ -2597,184 +2603,85 @@ class GatewayClient {
|
|
|
2597
2603
|
log._currentAssistantSegment._sealed = true;
|
|
2598
2604
|
}
|
|
2599
2605
|
// Remove the final assistant segment — that's the actual response, not narration
|
|
2600
|
-
// The last assistant segment is the final reply text, which is displayed as the message itself
|
|
2601
2606
|
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
2602
2607
|
if (lastAssistantIdx >= 0) {
|
|
2603
2608
|
log.steps.splice(lastAssistantIdx, 1);
|
|
2604
2609
|
}
|
|
2605
2610
|
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2611
|
+
// Write final state to DB, then clean up
|
|
2612
|
+
this._writeActivityToDb(runId, log);
|
|
2613
|
+
this.activityLogs.delete(runId);
|
|
2614
|
+
return;
|
|
2610
2615
|
}
|
|
2611
2616
|
}
|
|
2612
2617
|
}
|
|
2613
2618
|
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
type: 'clawchats',
|
|
2620
|
-
event: 'agent-activity',
|
|
2621
|
-
workspace: parsed.workspace,
|
|
2622
|
-
threadId: parsed.threadId,
|
|
2623
|
-
runId,
|
|
2624
|
-
steps: log.steps,
|
|
2625
|
-
summary: this.generateActivitySummary(log.steps)
|
|
2626
|
-
}));
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
finalizeActivityLog(runId, log) {
|
|
2630
|
-
const parsed = log.sessionKey ? parseSessionKey(log.sessionKey) : null;
|
|
2619
|
+
_writeActivityToDb(runId, log) {
|
|
2620
|
+
if (!log._parsed) {
|
|
2621
|
+
log._parsed = parseSessionKey(log.sessionKey);
|
|
2622
|
+
}
|
|
2623
|
+
const parsed = log._parsed;
|
|
2631
2624
|
if (!parsed) return;
|
|
2632
2625
|
|
|
2633
2626
|
const db = getDb(parsed.workspace);
|
|
2634
2627
|
if (!db) return;
|
|
2635
2628
|
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
// try to extract tool calls from the session history via Gateway API
|
|
2640
|
-
if (!hasToolSteps && this.ws && this.ws.readyState === 1 /* OPEN */) {
|
|
2641
|
-
this.fetchToolCallsFromHistory(log.sessionKey, parsed, db, log, runId);
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
// Save activity log to message metadata (delay to ensure final message is saved first)
|
|
2646
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
2647
|
-
}
|
|
2629
|
+
const cleanSteps = log.steps.map(s => { const c = {...s}; delete c._sealed; return c; });
|
|
2630
|
+
const summary = this.generateActivitySummary(log.steps);
|
|
2631
|
+
const now = Date.now();
|
|
2648
2632
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2633
|
+
if (!log._messageId) {
|
|
2634
|
+
// First write — INSERT the assistant message row
|
|
2635
|
+
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2636
|
+
if (!thread) return;
|
|
2651
2637
|
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
this._pendingActivityCallbacks?.delete(reqId);
|
|
2655
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
2656
|
-
}, 5000);
|
|
2638
|
+
const messageId = `gw-activity-${runId}`;
|
|
2639
|
+
const metadata = { activityLog: cleanSteps, activitySummary: summary, pending: true };
|
|
2657
2640
|
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2641
|
+
db.prepare(`
|
|
2642
|
+
INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at)
|
|
2643
|
+
VALUES (?, ?, 'assistant', '', 'sent', ?, ?, ?)
|
|
2644
|
+
`).run(messageId, parsed.threadId, JSON.stringify(metadata), now, now);
|
|
2662
2645
|
|
|
2663
|
-
|
|
2664
|
-
if (payload?.messages) {
|
|
2665
|
-
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
2666
|
-
const m = payload.messages[i];
|
|
2667
|
-
if (m.role === 'assistant' && Array.isArray(m.content)) {
|
|
2668
|
-
let hasToolUse = false;
|
|
2669
|
-
for (const block of m.content) {
|
|
2670
|
-
if (block.type === 'text' && block.text?.trim() && hasToolUse) {
|
|
2671
|
-
// Text after a tool_use = intermediate narration (not the first text which is part of the response)
|
|
2672
|
-
// Actually, text BEFORE a tool_use is narration too
|
|
2673
|
-
}
|
|
2674
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
2675
|
-
// We'll collect all text blocks, then trim the last one (that's the actual response)
|
|
2676
|
-
log.steps.push({
|
|
2677
|
-
type: 'assistant',
|
|
2678
|
-
timestamp: Date.now(),
|
|
2679
|
-
text: block.text.trim()
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
2682
|
-
if (block.type === 'tool_use') {
|
|
2683
|
-
hasToolUse = true;
|
|
2684
|
-
log.steps.push({
|
|
2685
|
-
type: 'tool',
|
|
2686
|
-
timestamp: Date.now(),
|
|
2687
|
-
name: block.name || 'unknown',
|
|
2688
|
-
phase: 'done',
|
|
2689
|
-
toolCallId: block.id,
|
|
2690
|
-
meta: this.extractToolMeta(block),
|
|
2691
|
-
isError: false
|
|
2692
|
-
});
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
// Remove the last assistant text — that's the final response, not narration
|
|
2696
|
-
if (hasToolUse) {
|
|
2697
|
-
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
2698
|
-
if (lastAssistantIdx >= 0) {
|
|
2699
|
-
log.steps.splice(lastAssistantIdx, 1);
|
|
2700
|
-
}
|
|
2701
|
-
} else {
|
|
2702
|
-
// No tool calls — remove all assistant narration (nothing interesting to show)
|
|
2703
|
-
log.steps = log.steps.filter(s => s.type !== 'assistant');
|
|
2704
|
-
}
|
|
2705
|
-
break; // Only process the last assistant message
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2646
|
+
log._messageId = messageId;
|
|
2709
2647
|
|
|
2710
|
-
|
|
2711
|
-
this.
|
|
2712
|
-
|
|
2648
|
+
// First event — broadcast message-saved so browser creates the message element
|
|
2649
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
2650
|
+
type: 'clawchats',
|
|
2651
|
+
event: 'message-saved',
|
|
2652
|
+
threadId: parsed.threadId,
|
|
2653
|
+
workspace: parsed.workspace,
|
|
2654
|
+
messageId,
|
|
2655
|
+
timestamp: now
|
|
2656
|
+
}));
|
|
2657
|
+
} else {
|
|
2658
|
+
// Subsequent writes — UPDATE metadata on existing row
|
|
2659
|
+
const existing = db.prepare('SELECT metadata FROM messages WHERE id = ?').get(log._messageId);
|
|
2660
|
+
const metadata = existing?.metadata ? JSON.parse(existing.metadata) : {};
|
|
2661
|
+
metadata.activityLog = cleanSteps;
|
|
2662
|
+
metadata.activitySummary = summary;
|
|
2663
|
+
metadata.pending = true;
|
|
2713
2664
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
id: reqId,
|
|
2718
|
-
method: 'sessions.history',
|
|
2719
|
-
params: { key: sessionKey, limit: 3, includeTools: true }
|
|
2720
|
-
}));
|
|
2665
|
+
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?')
|
|
2666
|
+
.run(JSON.stringify(metadata), log._messageId);
|
|
2667
|
+
}
|
|
2721
2668
|
}
|
|
2722
2669
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
// Generate a brief description from the tool input
|
|
2727
|
-
if (input.query) return input.query;
|
|
2728
|
-
if (input.command) return input.command.substring(0, 80);
|
|
2729
|
-
if (input.file_path || input.path) return input.file_path || input.path;
|
|
2730
|
-
if (input.url) return input.url;
|
|
2731
|
-
if (input.text) return input.text.substring(0, 60);
|
|
2732
|
-
return '';
|
|
2733
|
-
}
|
|
2670
|
+
_broadcastActivityUpdate(runId, log) {
|
|
2671
|
+
const parsed = log._parsed;
|
|
2672
|
+
if (!parsed || !log._messageId) return;
|
|
2734
2673
|
|
|
2735
|
-
|
|
2736
|
-
// Delay to ensure the assistant message is saved first (from handleChatEvent final)
|
|
2737
|
-
setTimeout(() => {
|
|
2738
|
-
try {
|
|
2739
|
-
const msg = db.prepare(
|
|
2740
|
-
'SELECT id, metadata FROM messages WHERE thread_id = ? AND role = ? ORDER BY timestamp DESC LIMIT 1'
|
|
2741
|
-
).get(parsed.threadId, 'assistant');
|
|
2742
|
-
|
|
2743
|
-
if (msg) {
|
|
2744
|
-
const toolSteps = log.steps.filter(s => s.type === 'tool' && s.phase !== 'update');
|
|
2745
|
-
const thinkingSteps = log.steps.filter(s => s.type === 'thinking');
|
|
2746
|
-
const narrativeSteps = log.steps.filter(s => s.type === 'assistant' && s.text?.trim());
|
|
2747
|
-
// Only save if we have meaningful activity
|
|
2748
|
-
if (toolSteps.length > 0 || thinkingSteps.length > 0 || narrativeSteps.length > 0) {
|
|
2749
|
-
const metadata = msg.metadata ? JSON.parse(msg.metadata) : {};
|
|
2750
|
-
// Clean internal fields before persisting
|
|
2751
|
-
metadata.activityLog = log.steps.map(s => {
|
|
2752
|
-
const clean = { ...s };
|
|
2753
|
-
delete clean._sealed;
|
|
2754
|
-
return clean;
|
|
2755
|
-
});
|
|
2756
|
-
metadata.activitySummary = this.generateActivitySummary(log.steps);
|
|
2757
|
-
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?')
|
|
2758
|
-
.run(JSON.stringify(metadata), msg.id);
|
|
2759
|
-
|
|
2760
|
-
console.log(`[ActivityLog] Saved ${toolSteps.length} tool steps for message ${msg.id}`);
|
|
2761
|
-
|
|
2762
|
-
// Notify browsers to re-render this message with activity data
|
|
2763
|
-
this.broadcastToBrowsers(JSON.stringify({
|
|
2764
|
-
type: 'clawchats',
|
|
2765
|
-
event: 'activity-saved',
|
|
2766
|
-
workspace: parsed.workspace,
|
|
2767
|
-
threadId: parsed.threadId,
|
|
2768
|
-
messageId: msg.id
|
|
2769
|
-
}));
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
} catch (e) {
|
|
2773
|
-
console.error('Failed to save activity log:', e.message);
|
|
2774
|
-
}
|
|
2674
|
+
const cleanSteps = log.steps.map(s => { const c = {...s}; delete c._sealed; return c; });
|
|
2775
2675
|
|
|
2776
|
-
|
|
2777
|
-
|
|
2676
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
2677
|
+
type: 'clawchats',
|
|
2678
|
+
event: 'activity-updated',
|
|
2679
|
+
workspace: parsed.workspace,
|
|
2680
|
+
threadId: parsed.threadId,
|
|
2681
|
+
messageId: log._messageId,
|
|
2682
|
+
activityLog: cleanSteps,
|
|
2683
|
+
activitySummary: this.generateActivitySummary(log.steps)
|
|
2684
|
+
}));
|
|
2778
2685
|
}
|
|
2779
2686
|
|
|
2780
2687
|
generateActivitySummary(steps) {
|
|
@@ -3600,7 +3507,19 @@ export function createApp(config = {}) {
|
|
|
3600
3507
|
setInterval(() => {
|
|
3601
3508
|
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
3602
3509
|
for (const [runId, log] of this.activityLogs) {
|
|
3603
|
-
if (log.startTime < cutoff)
|
|
3510
|
+
if (log.startTime < cutoff) {
|
|
3511
|
+
if (log._messageId) {
|
|
3512
|
+
const db = _getDb(log._parsed?.workspace);
|
|
3513
|
+
if (db) {
|
|
3514
|
+
db.prepare(`
|
|
3515
|
+
UPDATE messages SET content = '[Response interrupted]',
|
|
3516
|
+
metadata = json_remove(metadata, '$.pending')
|
|
3517
|
+
WHERE id = ? AND content = ''
|
|
3518
|
+
`).run(log._messageId);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
this.activityLogs.delete(runId);
|
|
3522
|
+
}
|
|
3604
3523
|
}
|
|
3605
3524
|
}, 5 * 60 * 1000);
|
|
3606
3525
|
}
|
|
@@ -3632,11 +3551,6 @@ export function createApp(config = {}) {
|
|
|
3632
3551
|
return;
|
|
3633
3552
|
}
|
|
3634
3553
|
if (msg.type === 'res' && msg.payload?.type === 'hello-ok') { console.log('Gateway handshake complete'); this.connected = true; this.broadcastGatewayStatus(true); }
|
|
3635
|
-
if (msg.type === 'res' && msg.id && this._pendingActivityCallbacks?.has(msg.id)) {
|
|
3636
|
-
const callback = this._pendingActivityCallbacks.get(msg.id);
|
|
3637
|
-
this._pendingActivityCallbacks.delete(msg.id);
|
|
3638
|
-
if (msg.ok) callback(msg.payload); else callback(null);
|
|
3639
|
-
}
|
|
3640
3554
|
this.broadcastToBrowsers(data);
|
|
3641
3555
|
if (msg.type === 'event' && msg.event === 'chat' && msg.payload) this.handleChatEvent(msg.payload);
|
|
3642
3556
|
if (msg.type === 'event' && msg.event === 'agent' && msg.payload) this.handleAgentEvent(msg.payload);
|
|
@@ -3669,11 +3583,37 @@ export function createApp(config = {}) {
|
|
|
3669
3583
|
const content = extractContent(message);
|
|
3670
3584
|
if (!content || !content.trim()) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
3671
3585
|
const now = Date.now();
|
|
3672
|
-
|
|
3673
|
-
|
|
3586
|
+
|
|
3587
|
+
// Check for pending activity message
|
|
3588
|
+
const pendingMsg = db.prepare(`
|
|
3589
|
+
SELECT id, metadata FROM messages
|
|
3590
|
+
WHERE thread_id = ? AND role = 'assistant'
|
|
3591
|
+
AND json_extract(metadata, '$.pending') = 1
|
|
3592
|
+
ORDER BY timestamp DESC LIMIT 1
|
|
3593
|
+
`).get(parsed.threadId);
|
|
3594
|
+
|
|
3595
|
+
let messageId;
|
|
3596
|
+
|
|
3597
|
+
if (pendingMsg) {
|
|
3598
|
+
// Merge final content into existing activity row
|
|
3599
|
+
const metadata = pendingMsg.metadata ? JSON.parse(pendingMsg.metadata) : {};
|
|
3600
|
+
delete metadata.pending;
|
|
3601
|
+
if (metadata.activityLog) {
|
|
3602
|
+
const lastAssistantIdx = metadata.activityLog.findLastIndex(s => s.type === 'assistant');
|
|
3603
|
+
if (lastAssistantIdx >= 0) metadata.activityLog.splice(lastAssistantIdx, 1);
|
|
3604
|
+
metadata.activitySummary = this.generateActivitySummary(metadata.activityLog);
|
|
3605
|
+
}
|
|
3606
|
+
db.prepare('UPDATE messages SET content = ?, metadata = ?, timestamp = ? WHERE id = ?')
|
|
3607
|
+
.run(content, JSON.stringify(metadata), now, pendingMsg.id);
|
|
3608
|
+
messageId = pendingMsg.id;
|
|
3609
|
+
} else {
|
|
3610
|
+
// No pending activity — normal INSERT (simple responses, no tools)
|
|
3611
|
+
messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
3674
3612
|
db.prepare(`INSERT INTO messages (id, thread_id, role, content, status, timestamp, created_at) VALUES (?, ?, 'assistant', ?, 'sent', ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, timestamp = excluded.timestamp`).run(messageId, parsed.threadId, content, now, now);
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
try {
|
|
3675
3616
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
3676
|
-
// Always mark as unread — browser sends read receipts to clear
|
|
3677
3617
|
db.prepare('INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)').run(parsed.threadId, messageId, now);
|
|
3678
3618
|
syncThreadUnreadCount(db, parsed.threadId);
|
|
3679
3619
|
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId);
|
|
@@ -3682,7 +3622,7 @@ export function createApp(config = {}) {
|
|
|
3682
3622
|
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'message-saved', threadId: parsed.threadId, workspace: parsed.workspace, messageId, timestamp: now, title: threadInfo?.title || 'Chat', preview, unreadCount }));
|
|
3683
3623
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
3684
3624
|
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'unread-update', workspace: parsed.workspace, threadId: parsed.threadId, messageId, action: 'new', unreadCount, workspaceUnreadTotal, title: threadInfo?.title || 'Chat', preview, timestamp: now }));
|
|
3685
|
-
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (seq:
|
|
3625
|
+
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (${pendingMsg ? 'merged into pending' : 'seq: ' + seq})`);
|
|
3686
3626
|
} catch (e) { console.error(`Failed to save assistant message:`, e.message); }
|
|
3687
3627
|
}
|
|
3688
3628
|
|
|
@@ -3726,14 +3666,15 @@ export function createApp(config = {}) {
|
|
|
3726
3666
|
let thinkingStep = log.steps.find(s => s.type === 'thinking');
|
|
3727
3667
|
if (thinkingStep) { thinkingStep.text = thinkingText; }
|
|
3728
3668
|
else { log.steps.push({ type: 'thinking', timestamp: Date.now(), text: thinkingText }); }
|
|
3669
|
+
this._writeActivityToDb(runId, log);
|
|
3729
3670
|
const now = Date.now();
|
|
3730
3671
|
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) {
|
|
3731
3672
|
log._lastThinkingBroadcast = now;
|
|
3732
|
-
this.
|
|
3673
|
+
this._broadcastActivityUpdate(runId, log);
|
|
3733
3674
|
}
|
|
3734
3675
|
}
|
|
3735
3676
|
if (stream === 'tool') {
|
|
3736
|
-
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) { log._currentAssistantSegment._sealed = true;
|
|
3677
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) { log._currentAssistantSegment._sealed = true; }
|
|
3737
3678
|
const step = { type: 'tool', timestamp: Date.now(), name: data?.name || 'unknown', phase: data?.phase || 'start', toolCallId: data?.toolCallId, meta: data?.meta, isError: data?.isError || false };
|
|
3738
3679
|
if (data?.phase === 'result') {
|
|
3739
3680
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
|
|
@@ -3743,93 +3684,84 @@ export function createApp(config = {}) {
|
|
|
3743
3684
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId);
|
|
3744
3685
|
if (existing) { if (data?.meta) existing.resultMeta = data.meta; if (data?.isError) existing.isError = true; existing.phase = 'running'; }
|
|
3745
3686
|
} else { log.steps.push(step); }
|
|
3746
|
-
this.
|
|
3687
|
+
this._writeActivityToDb(runId, log);
|
|
3688
|
+
this._broadcastActivityUpdate(runId, log);
|
|
3747
3689
|
}
|
|
3748
3690
|
if (stream === 'lifecycle') {
|
|
3749
3691
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
3750
3692
|
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
|
|
3751
3693
|
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
3752
3694
|
if (lastAssistantIdx >= 0) log.steps.splice(lastAssistantIdx, 1);
|
|
3753
|
-
|
|
3754
|
-
this.
|
|
3695
|
+
this._writeActivityToDb(runId, log);
|
|
3696
|
+
this.activityLogs.delete(runId);
|
|
3697
|
+
return;
|
|
3755
3698
|
}
|
|
3756
3699
|
}
|
|
3757
3700
|
}
|
|
3758
3701
|
|
|
3759
|
-
|
|
3760
|
-
|
|
3702
|
+
_writeActivityToDb(runId, log) {
|
|
3703
|
+
if (!log._parsed) {
|
|
3704
|
+
log._parsed = parseSessionKey(log.sessionKey);
|
|
3705
|
+
}
|
|
3706
|
+
const parsed = log._parsed;
|
|
3761
3707
|
if (!parsed) return;
|
|
3762
|
-
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'agent-activity', workspace: parsed.workspace, threadId: parsed.threadId, runId, steps: log.steps, summary: this.generateActivitySummary(log.steps) }));
|
|
3763
|
-
}
|
|
3764
3708
|
|
|
3765
|
-
finalizeActivityLog(runId, log) {
|
|
3766
|
-
const parsed = log.sessionKey ? parseSessionKey(log.sessionKey) : null;
|
|
3767
|
-
if (!parsed) return;
|
|
3768
3709
|
const db = _getDb(parsed.workspace);
|
|
3769
3710
|
if (!db) return;
|
|
3770
|
-
const hasToolSteps = log.steps.some(s => s.type === 'tool');
|
|
3771
|
-
if (!hasToolSteps && this.ws && this.ws.readyState === 1) { this.fetchToolCallsFromHistory(log.sessionKey, parsed, db, log, runId); return; }
|
|
3772
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
fetchToolCallsFromHistory(sessionKey, parsed, db, log, runId) {
|
|
3776
|
-
const reqId = `activity-hist-${runId}`;
|
|
3777
|
-
const timeout = setTimeout(() => { this._pendingActivityCallbacks?.delete(reqId); this.saveActivityToMessage(parsed, db, log, runId); }, 5000);
|
|
3778
|
-
if (!this._pendingActivityCallbacks) this._pendingActivityCallbacks = new Map();
|
|
3779
|
-
this._pendingActivityCallbacks.set(reqId, (payload) => {
|
|
3780
|
-
clearTimeout(timeout);
|
|
3781
|
-
if (payload?.messages) {
|
|
3782
|
-
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
3783
|
-
const m = payload.messages[i];
|
|
3784
|
-
if (m.role === 'assistant' && Array.isArray(m.content)) {
|
|
3785
|
-
let hasToolUse = false;
|
|
3786
|
-
for (const block of m.content) {
|
|
3787
|
-
if (block.type === 'text' && block.text?.trim()) log.steps.push({ type: 'assistant', timestamp: Date.now(), text: block.text.trim() });
|
|
3788
|
-
if (block.type === 'tool_use') { hasToolUse = true; log.steps.push({ type: 'tool', timestamp: Date.now(), name: block.name || 'unknown', phase: 'done', toolCallId: block.id, meta: this.extractToolMeta(block), isError: false }); }
|
|
3789
|
-
}
|
|
3790
|
-
if (hasToolUse) { const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant'); if (lastAssistantIdx >= 0) log.steps.splice(lastAssistantIdx, 1); }
|
|
3791
|
-
else log.steps = log.steps.filter(s => s.type !== 'assistant');
|
|
3792
|
-
break;
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
3797
|
-
this.broadcastActivityUpdate(runId, log);
|
|
3798
|
-
});
|
|
3799
|
-
this.ws.send(JSON.stringify({ type: 'req', id: reqId, method: 'sessions.history', params: { key: sessionKey, limit: 3, includeTools: true } }));
|
|
3800
|
-
}
|
|
3801
3711
|
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
const
|
|
3805
|
-
|
|
3806
|
-
if (
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3712
|
+
const cleanSteps = log.steps.map(s => { const c = {...s}; delete c._sealed; return c; });
|
|
3713
|
+
const summary = this.generateActivitySummary(log.steps);
|
|
3714
|
+
const now = Date.now();
|
|
3715
|
+
|
|
3716
|
+
if (!log._messageId) {
|
|
3717
|
+
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
3718
|
+
if (!thread) return;
|
|
3719
|
+
|
|
3720
|
+
const messageId = `gw-activity-${runId}`;
|
|
3721
|
+
const metadata = { activityLog: cleanSteps, activitySummary: summary, pending: true };
|
|
3722
|
+
|
|
3723
|
+
db.prepare(`
|
|
3724
|
+
INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at)
|
|
3725
|
+
VALUES (?, ?, 'assistant', '', 'sent', ?, ?, ?)
|
|
3726
|
+
`).run(messageId, parsed.threadId, JSON.stringify(metadata), now, now);
|
|
3727
|
+
|
|
3728
|
+
log._messageId = messageId;
|
|
3729
|
+
|
|
3730
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
3731
|
+
type: 'clawchats',
|
|
3732
|
+
event: 'message-saved',
|
|
3733
|
+
threadId: parsed.threadId,
|
|
3734
|
+
workspace: parsed.workspace,
|
|
3735
|
+
messageId,
|
|
3736
|
+
timestamp: now
|
|
3737
|
+
}));
|
|
3738
|
+
} else {
|
|
3739
|
+
const existing = db.prepare('SELECT metadata FROM messages WHERE id = ?').get(log._messageId);
|
|
3740
|
+
const metadata = existing?.metadata ? JSON.parse(existing.metadata) : {};
|
|
3741
|
+
metadata.activityLog = cleanSteps;
|
|
3742
|
+
metadata.activitySummary = summary;
|
|
3743
|
+
metadata.pending = true;
|
|
3744
|
+
|
|
3745
|
+
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?')
|
|
3746
|
+
.run(JSON.stringify(metadata), log._messageId);
|
|
3747
|
+
}
|
|
3811
3748
|
}
|
|
3812
3749
|
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
} catch (e) { console.error('Failed to save activity log:', e.message); }
|
|
3831
|
-
this.activityLogs.delete(runId);
|
|
3832
|
-
}, 1000);
|
|
3750
|
+
_broadcastActivityUpdate(runId, log) {
|
|
3751
|
+
const parsed = log._parsed;
|
|
3752
|
+
if (!parsed || !log._messageId) return;
|
|
3753
|
+
|
|
3754
|
+
const cleanSteps = log.steps.map(s => { const c = {...s}; delete c._sealed; return c; });
|
|
3755
|
+
|
|
3756
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
3757
|
+
type: 'clawchats',
|
|
3758
|
+
event: 'activity-updated',
|
|
3759
|
+
workspace: parsed.workspace,
|
|
3760
|
+
threadId: parsed.threadId,
|
|
3761
|
+
messageId: log._messageId,
|
|
3762
|
+
activityLog: cleanSteps,
|
|
3763
|
+
activitySummary: this.generateActivitySummary(log.steps)
|
|
3764
|
+
}));
|
|
3833
3765
|
}
|
|
3834
3766
|
|
|
3835
3767
|
generateActivitySummary(steps) {
|