@clawchatsai/connector 0.0.29 → 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 +335 -336
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -368,13 +368,6 @@ function migrate(db) {
|
|
|
368
368
|
`);
|
|
369
369
|
db.exec('CREATE INDEX IF NOT EXISTS idx_unread_thread ON unread_messages(thread_id)');
|
|
370
370
|
|
|
371
|
-
// Migration: add type column for segment model (text/tool/thinking)
|
|
372
|
-
try {
|
|
373
|
-
db.exec("ALTER TABLE messages ADD COLUMN type TEXT DEFAULT 'text'");
|
|
374
|
-
} catch (e) {
|
|
375
|
-
// Column already exists — ignore
|
|
376
|
-
}
|
|
377
|
-
|
|
378
371
|
// FTS5 table — CREATE VIRTUAL TABLE doesn't support IF NOT EXISTS in all versions,
|
|
379
372
|
// so check if it exists first
|
|
380
373
|
const hasFts = db.prepare(
|
|
@@ -2233,14 +2226,24 @@ class GatewayClient {
|
|
|
2233
2226
|
this.maxReconnectDelay = 30000;
|
|
2234
2227
|
this.browserClients = new Map(); // Map<WebSocket, { activeWorkspace, activeThreadId }>
|
|
2235
2228
|
this.streamState = new Map(); // Map<sessionKey, { state, buffer, threadId }>
|
|
2236
|
-
this.
|
|
2229
|
+
this.activityLogs = new Map(); // Map<runId, { sessionKey, steps, startTime }>
|
|
2237
2230
|
|
|
2238
|
-
// Clean up stale
|
|
2231
|
+
// Clean up stale activity logs every 5 minutes (runs that never completed)
|
|
2239
2232
|
setInterval(() => {
|
|
2240
|
-
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
2241
|
-
for (const [runId,
|
|
2242
|
-
if (
|
|
2243
|
-
|
|
2233
|
+
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
2234
|
+
for (const [runId, log] of this.activityLogs) {
|
|
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
|
+
}
|
|
2246
|
+
this.activityLogs.delete(runId);
|
|
2244
2247
|
}
|
|
2245
2248
|
}
|
|
2246
2249
|
}, 5 * 60 * 1000);
|
|
@@ -2354,10 +2357,9 @@ class GatewayClient {
|
|
|
2354
2357
|
this.streamState.delete(sessionKey);
|
|
2355
2358
|
}
|
|
2356
2359
|
|
|
2357
|
-
// Save assistant messages on final
|
|
2360
|
+
// Save assistant messages on final
|
|
2358
2361
|
if (state === 'final') {
|
|
2359
|
-
|
|
2360
|
-
this.saveAssistantMessage(sessionKey, message, seq, run?.finalSeq);
|
|
2362
|
+
this.saveAssistantMessage(sessionKey, message, seq);
|
|
2361
2363
|
}
|
|
2362
2364
|
|
|
2363
2365
|
// Save error markers
|
|
@@ -2366,11 +2368,10 @@ class GatewayClient {
|
|
|
2366
2368
|
}
|
|
2367
2369
|
}
|
|
2368
2370
|
|
|
2369
|
-
saveAssistantMessage(sessionKey, message, seq
|
|
2371
|
+
saveAssistantMessage(sessionKey, message, seq) {
|
|
2370
2372
|
const parsed = parseSessionKey(sessionKey);
|
|
2371
|
-
if (!parsed) return;
|
|
2373
|
+
if (!parsed) return;
|
|
2372
2374
|
|
|
2373
|
-
// Guard: verify workspace still exists
|
|
2374
2375
|
const ws = getWorkspaces();
|
|
2375
2376
|
if (!ws.workspaces[parsed.workspace]) {
|
|
2376
2377
|
console.log(`Ignoring response for deleted workspace: ${parsed.workspace}`);
|
|
@@ -2379,52 +2380,66 @@ class GatewayClient {
|
|
|
2379
2380
|
|
|
2380
2381
|
const db = getDb(parsed.workspace);
|
|
2381
2382
|
|
|
2382
|
-
// Guard: verify thread still exists
|
|
2383
2383
|
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2384
2384
|
if (!thread) {
|
|
2385
2385
|
console.log(`Ignoring response for deleted thread: ${parsed.threadId}`);
|
|
2386
2386
|
return;
|
|
2387
2387
|
}
|
|
2388
2388
|
|
|
2389
|
-
// Extract content
|
|
2390
2389
|
const content = extractContent(message);
|
|
2391
|
-
|
|
2392
|
-
// Guard: skip empty content
|
|
2393
2390
|
if (!content || !content.trim()) {
|
|
2394
2391
|
console.log(`Skipping empty assistant response for thread ${parsed.threadId}`);
|
|
2395
2392
|
return;
|
|
2396
2393
|
}
|
|
2397
2394
|
|
|
2398
|
-
// Deterministic message ID from seq (deduplicates tool-call loops)
|
|
2399
2395
|
const now = Date.now();
|
|
2400
|
-
const messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
2401
2396
|
|
|
2402
|
-
//
|
|
2403
|
-
const
|
|
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
|
+
}
|
|
2404
2418
|
|
|
2405
|
-
|
|
2406
|
-
|
|
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}`;
|
|
2407
2426
|
db.prepare(`
|
|
2408
|
-
INSERT INTO messages (id, thread_id, role,
|
|
2409
|
-
VALUES (?, ?, 'assistant',
|
|
2410
|
-
ON CONFLICT(id) DO UPDATE SET content = excluded.content,
|
|
2411
|
-
`).run(messageId, parsed.threadId, content,
|
|
2427
|
+
INSERT INTO messages (id, thread_id, role, content, status, timestamp, created_at)
|
|
2428
|
+
VALUES (?, ?, 'assistant', ?, 'sent', ?, ?)
|
|
2429
|
+
ON CONFLICT(id) DO UPDATE SET content = excluded.content, timestamp = excluded.timestamp
|
|
2430
|
+
`).run(messageId, parsed.threadId, content, now, now);
|
|
2431
|
+
}
|
|
2412
2432
|
|
|
2413
|
-
|
|
2433
|
+
// Thread timestamp + unreads + broadcast (same for both paths)
|
|
2434
|
+
try {
|
|
2414
2435
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
2415
|
-
|
|
2416
|
-
// Always mark as unread server-side. The browser client is responsible
|
|
2417
|
-
// for sending read receipts (active-thread) to clear unreads — same
|
|
2418
|
-
// pattern as Slack/Discord. Server doesn't track viewport state.
|
|
2419
2436
|
db.prepare('INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)').run(parsed.threadId, messageId, now);
|
|
2420
2437
|
syncThreadUnreadCount(db, parsed.threadId);
|
|
2421
2438
|
|
|
2422
|
-
// Get thread title and unread info for notification
|
|
2423
2439
|
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2424
2440
|
const unreadCount = db.prepare('SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?').get(parsed.threadId).c;
|
|
2425
2441
|
const preview = content.length > 120 ? content.substring(0, 120) + '...' : content;
|
|
2426
2442
|
|
|
2427
|
-
// Broadcast message-saved for active thread reload
|
|
2428
2443
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2429
2444
|
type: 'clawchats',
|
|
2430
2445
|
event: 'message-saved',
|
|
@@ -2437,7 +2452,6 @@ class GatewayClient {
|
|
|
2437
2452
|
unreadCount
|
|
2438
2453
|
}));
|
|
2439
2454
|
|
|
2440
|
-
// Always broadcast unread-update — browser sends read receipts to clear
|
|
2441
2455
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
2442
2456
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2443
2457
|
type: 'clawchats',
|
|
@@ -2453,7 +2467,7 @@ class GatewayClient {
|
|
|
2453
2467
|
timestamp: now
|
|
2454
2468
|
}));
|
|
2455
2469
|
|
|
2456
|
-
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})`);
|
|
2457
2471
|
} catch (e) {
|
|
2458
2472
|
console.error(`Failed to save assistant message:`, e.message);
|
|
2459
2473
|
}
|
|
@@ -2490,242 +2504,183 @@ class GatewayClient {
|
|
|
2490
2504
|
const { runId, stream, data, sessionKey } = payload;
|
|
2491
2505
|
if (!runId) return;
|
|
2492
2506
|
|
|
2493
|
-
// Initialize
|
|
2494
|
-
if (!this.
|
|
2495
|
-
this.
|
|
2507
|
+
// Initialize log if needed
|
|
2508
|
+
if (!this.activityLogs.has(runId)) {
|
|
2509
|
+
this.activityLogs.set(runId, { sessionKey, steps: [], startTime: Date.now() });
|
|
2496
2510
|
}
|
|
2497
|
-
const
|
|
2498
|
-
|
|
2499
|
-
const parsed = parseSessionKey(sessionKey);
|
|
2500
|
-
if (!parsed) return;
|
|
2501
|
-
|
|
2502
|
-
// Verify workspace/thread still exist
|
|
2503
|
-
const ws = getWorkspaces();
|
|
2504
|
-
if (!ws.workspaces[parsed.workspace]) return;
|
|
2505
|
-
const db = getDb(parsed.workspace);
|
|
2506
|
-
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
2507
|
-
if (!thread) return;
|
|
2511
|
+
const log = this.activityLogs.get(runId);
|
|
2508
2512
|
|
|
2509
2513
|
if (stream === 'assistant') {
|
|
2514
|
+
// Capture intermediate text turns (narration between tool calls)
|
|
2510
2515
|
const text = data?.text || '';
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
// Broadcast segment update for live display (throttled ~150ms)
|
|
2526
|
-
const now = Date.now();
|
|
2527
|
-
if (!run.lastTextBroadcast || now - run.lastTextBroadcast >= 150) {
|
|
2528
|
-
run.lastTextBroadcast = now;
|
|
2529
|
-
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2530
|
-
id: run.currentTextSegmentId,
|
|
2531
|
-
type: 'text',
|
|
2532
|
-
seq: run.currentTextSeq,
|
|
2533
|
-
content: run.currentTextContent
|
|
2534
|
-
});
|
|
2516
|
+
if (text) {
|
|
2517
|
+
let currentSegment = log._currentAssistantSegment;
|
|
2518
|
+
if (!currentSegment || currentSegment._sealed) {
|
|
2519
|
+
currentSegment = {
|
|
2520
|
+
type: 'assistant',
|
|
2521
|
+
timestamp: Date.now(),
|
|
2522
|
+
text: text,
|
|
2523
|
+
_sealed: false
|
|
2524
|
+
};
|
|
2525
|
+
log._currentAssistantSegment = currentSegment;
|
|
2526
|
+
log.steps.push(currentSegment);
|
|
2527
|
+
} else {
|
|
2528
|
+
currentSegment.text = text;
|
|
2529
|
+
}
|
|
2535
2530
|
}
|
|
2531
|
+
// Don't broadcast on every assistant delta — too noisy
|
|
2536
2532
|
return;
|
|
2537
2533
|
}
|
|
2538
2534
|
|
|
2539
2535
|
if (stream === 'thinking') {
|
|
2540
2536
|
const thinkingText = data?.text || '';
|
|
2541
|
-
|
|
2542
|
-
if (
|
|
2543
|
-
|
|
2544
|
-
const segId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
2545
|
-
run.thinkingSegmentId = segId;
|
|
2546
|
-
run.thinkingSeq = run.seqCounter;
|
|
2547
|
-
const now = Date.now();
|
|
2548
|
-
try {
|
|
2549
|
-
db.prepare(`
|
|
2550
|
-
INSERT INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at)
|
|
2551
|
-
VALUES (?, ?, 'assistant', 'thinking', ?, 'sent', ?, ?, ?)
|
|
2552
|
-
`).run(segId, parsed.threadId, thinkingText, run.seqCounter, now, now);
|
|
2553
|
-
} catch (e) {
|
|
2554
|
-
console.error('Failed to save thinking segment:', e.message);
|
|
2555
|
-
}
|
|
2537
|
+
let thinkingStep = log.steps.find(s => s.type === 'thinking');
|
|
2538
|
+
if (thinkingStep) {
|
|
2539
|
+
thinkingStep.text = thinkingText;
|
|
2556
2540
|
} else {
|
|
2557
|
-
|
|
2558
|
-
db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(thinkingText, run.thinkingSegmentId);
|
|
2559
|
-
} catch (e) {
|
|
2560
|
-
console.error('Failed to update thinking segment:', e.message);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
// Broadcast (throttled ~300ms)
|
|
2565
|
-
const now = Date.now();
|
|
2566
|
-
if (!run.lastThinkingBroadcast || now - run.lastThinkingBroadcast >= 300) {
|
|
2567
|
-
run.lastThinkingBroadcast = now;
|
|
2568
|
-
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2569
|
-
id: run.thinkingSegmentId,
|
|
2541
|
+
log.steps.push({
|
|
2570
2542
|
type: 'thinking',
|
|
2571
|
-
|
|
2572
|
-
|
|
2543
|
+
timestamp: Date.now(),
|
|
2544
|
+
text: thinkingText
|
|
2573
2545
|
});
|
|
2574
2546
|
}
|
|
2575
|
-
|
|
2547
|
+
// Always write to DB; throttle broadcasts to every 300ms
|
|
2548
|
+
this._writeActivityToDb(runId, log);
|
|
2549
|
+
const now = Date.now();
|
|
2550
|
+
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) {
|
|
2551
|
+
log._lastThinkingBroadcast = now;
|
|
2552
|
+
this._broadcastActivityUpdate(runId, log);
|
|
2553
|
+
}
|
|
2576
2554
|
}
|
|
2577
2555
|
|
|
2578
2556
|
if (stream === 'tool') {
|
|
2579
|
-
// Seal
|
|
2580
|
-
if (
|
|
2581
|
-
|
|
2582
|
-
const content = run.currentTextContent;
|
|
2583
|
-
const seq = run.currentTextSeq;
|
|
2584
|
-
const ts = run.currentTextTimestamp || Date.now();
|
|
2585
|
-
try {
|
|
2586
|
-
db.prepare(`
|
|
2587
|
-
INSERT OR REPLACE INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at)
|
|
2588
|
-
VALUES (?, ?, 'assistant', 'text', ?, 'sent', ?, ?, ?)
|
|
2589
|
-
`).run(segId, parsed.threadId, content, seq, ts, ts);
|
|
2590
|
-
} catch (e) {
|
|
2591
|
-
console.error('Failed to save sealed text segment:', e.message);
|
|
2592
|
-
}
|
|
2557
|
+
// Seal any current assistant text segment (narration before this tool call)
|
|
2558
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
2559
|
+
log._currentAssistantSegment._sealed = true;
|
|
2593
2560
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
try {
|
|
2617
|
-
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), existingId);
|
|
2618
|
-
} catch (e) {
|
|
2619
|
-
console.error('Failed to update tool segment result:', e.message);
|
|
2620
|
-
}
|
|
2621
|
-
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2622
|
-
id: existingId,
|
|
2623
|
-
type: 'tool',
|
|
2624
|
-
seq: run.toolSeqMap.get(toolCallId),
|
|
2625
|
-
metadata
|
|
2626
|
-
});
|
|
2561
|
+
|
|
2562
|
+
const step = {
|
|
2563
|
+
type: 'tool',
|
|
2564
|
+
timestamp: Date.now(),
|
|
2565
|
+
name: data?.name || 'unknown',
|
|
2566
|
+
phase: data?.phase || 'start',
|
|
2567
|
+
toolCallId: data?.toolCallId,
|
|
2568
|
+
meta: data?.meta,
|
|
2569
|
+
isError: data?.isError || false
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
// On result phase, update the existing start step or add new
|
|
2573
|
+
if (data?.phase === 'result') {
|
|
2574
|
+
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
|
|
2575
|
+
if (existing) {
|
|
2576
|
+
existing.phase = 'done';
|
|
2577
|
+
existing.resultMeta = data?.meta;
|
|
2578
|
+
existing.isError = data?.isError || false;
|
|
2579
|
+
existing.durationMs = Date.now() - existing.timestamp;
|
|
2580
|
+
} else {
|
|
2581
|
+
step.phase = 'done';
|
|
2582
|
+
log.steps.push(step);
|
|
2627
2583
|
}
|
|
2628
|
-
} else if (phase === 'update') {
|
|
2629
|
-
const
|
|
2630
|
-
if (
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
meta: data?.meta || ''
|
|
2635
|
-
};
|
|
2636
|
-
try {
|
|
2637
|
-
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), existingId);
|
|
2638
|
-
} catch (e) {
|
|
2639
|
-
console.error('Failed to update tool segment:', e.message);
|
|
2640
|
-
}
|
|
2641
|
-
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2642
|
-
id: existingId,
|
|
2643
|
-
type: 'tool',
|
|
2644
|
-
seq: run.toolSeqMap.get(toolCallId),
|
|
2645
|
-
metadata
|
|
2646
|
-
});
|
|
2584
|
+
} else if (data?.phase === 'update') {
|
|
2585
|
+
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId);
|
|
2586
|
+
if (existing) {
|
|
2587
|
+
if (data?.meta) existing.resultMeta = data.meta;
|
|
2588
|
+
if (data?.isError) existing.isError = true;
|
|
2589
|
+
existing.phase = 'running';
|
|
2647
2590
|
}
|
|
2648
2591
|
} else {
|
|
2649
|
-
|
|
2650
|
-
run.seqCounter++;
|
|
2651
|
-
const segId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
2652
|
-
const now = Date.now();
|
|
2653
|
-
const metadata = {
|
|
2654
|
-
name: data?.name || 'unknown',
|
|
2655
|
-
status: 'running',
|
|
2656
|
-
meta: data?.meta || ''
|
|
2657
|
-
};
|
|
2658
|
-
try {
|
|
2659
|
-
db.prepare(`
|
|
2660
|
-
INSERT INTO messages (id, thread_id, role, type, content, status, metadata, seq, timestamp, created_at)
|
|
2661
|
-
VALUES (?, ?, 'assistant', 'tool', '', 'sent', ?, ?, ?, ?)
|
|
2662
|
-
`).run(segId, parsed.threadId, JSON.stringify(metadata), run.seqCounter, now, now);
|
|
2663
|
-
} catch (e) {
|
|
2664
|
-
console.error('Failed to save tool segment:', e.message);
|
|
2665
|
-
}
|
|
2666
|
-
run.toolSegmentMap.set(toolCallId, segId);
|
|
2667
|
-
run.toolSeqMap.set(toolCallId, run.seqCounter);
|
|
2668
|
-
run.toolStartTimes.set(toolCallId, now);
|
|
2669
|
-
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2670
|
-
id: segId,
|
|
2671
|
-
type: 'tool',
|
|
2672
|
-
seq: run.seqCounter,
|
|
2673
|
-
metadata
|
|
2674
|
-
});
|
|
2592
|
+
log.steps.push(step);
|
|
2675
2593
|
}
|
|
2676
|
-
|
|
2594
|
+
|
|
2595
|
+
this._writeActivityToDb(runId, log);
|
|
2596
|
+
this._broadcastActivityUpdate(runId, log);
|
|
2677
2597
|
}
|
|
2678
2598
|
|
|
2679
2599
|
if (stream === 'lifecycle') {
|
|
2680
2600
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
2681
|
-
//
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
});
|
|
2690
|
-
// Store seq for handleChatEvent.final to position final message correctly
|
|
2691
|
-
run.finalSeq = run.currentTextSeq;
|
|
2692
|
-
run.currentTextSegmentId = null;
|
|
2693
|
-
run.currentTextContent = null;
|
|
2694
|
-
} else {
|
|
2695
|
-
// No active text segment — final seq is next position
|
|
2696
|
-
run.finalSeq = run.seqCounter + 1;
|
|
2601
|
+
// Seal any remaining assistant segment
|
|
2602
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) {
|
|
2603
|
+
log._currentAssistantSegment._sealed = true;
|
|
2604
|
+
}
|
|
2605
|
+
// Remove the final assistant segment — that's the actual response, not narration
|
|
2606
|
+
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
2607
|
+
if (lastAssistantIdx >= 0) {
|
|
2608
|
+
log.steps.splice(lastAssistantIdx, 1);
|
|
2697
2609
|
}
|
|
2698
2610
|
|
|
2699
|
-
//
|
|
2700
|
-
this.
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
workspace: parsed.workspace,
|
|
2704
|
-
threadId: parsed.threadId,
|
|
2705
|
-
runId
|
|
2706
|
-
}));
|
|
2707
|
-
|
|
2708
|
-
// Keep run state briefly for handleChatEvent.final to read finalSeq
|
|
2709
|
-
setTimeout(() => this.runState.delete(runId), 30000);
|
|
2611
|
+
// Write final state to DB, then clean up
|
|
2612
|
+
this._writeActivityToDb(runId, log);
|
|
2613
|
+
this.activityLogs.delete(runId);
|
|
2614
|
+
return;
|
|
2710
2615
|
}
|
|
2711
2616
|
}
|
|
2712
2617
|
}
|
|
2713
2618
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2619
|
+
_writeActivityToDb(runId, log) {
|
|
2620
|
+
if (!log._parsed) {
|
|
2621
|
+
log._parsed = parseSessionKey(log.sessionKey);
|
|
2622
|
+
}
|
|
2623
|
+
const parsed = log._parsed;
|
|
2624
|
+
if (!parsed) return;
|
|
2625
|
+
|
|
2626
|
+
const db = getDb(parsed.workspace);
|
|
2627
|
+
if (!db) return;
|
|
2628
|
+
|
|
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();
|
|
2632
|
+
|
|
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;
|
|
2637
|
+
|
|
2638
|
+
const messageId = `gw-activity-${runId}`;
|
|
2639
|
+
const metadata = { activityLog: cleanSteps, activitySummary: summary, pending: true };
|
|
2640
|
+
|
|
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);
|
|
2645
|
+
|
|
2646
|
+
log._messageId = messageId;
|
|
2647
|
+
|
|
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;
|
|
2664
|
+
|
|
2665
|
+
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?')
|
|
2666
|
+
.run(JSON.stringify(metadata), log._messageId);
|
|
2717
2667
|
}
|
|
2718
|
-
return null;
|
|
2719
2668
|
}
|
|
2720
2669
|
|
|
2721
|
-
|
|
2670
|
+
_broadcastActivityUpdate(runId, log) {
|
|
2671
|
+
const parsed = log._parsed;
|
|
2672
|
+
if (!parsed || !log._messageId) return;
|
|
2673
|
+
|
|
2674
|
+
const cleanSteps = log.steps.map(s => { const c = {...s}; delete c._sealed; return c; });
|
|
2675
|
+
|
|
2722
2676
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2723
2677
|
type: 'clawchats',
|
|
2724
|
-
event: '
|
|
2678
|
+
event: 'activity-updated',
|
|
2725
2679
|
workspace: parsed.workspace,
|
|
2726
2680
|
threadId: parsed.threadId,
|
|
2727
|
-
|
|
2728
|
-
|
|
2681
|
+
messageId: log._messageId,
|
|
2682
|
+
activityLog: cleanSteps,
|
|
2683
|
+
activitySummary: this.generateActivitySummary(log.steps)
|
|
2729
2684
|
}));
|
|
2730
2685
|
}
|
|
2731
2686
|
|
|
@@ -3548,11 +3503,23 @@ export function createApp(config = {}) {
|
|
|
3548
3503
|
this.browserClients = new Map();
|
|
3549
3504
|
this._externalBroadcastTargets = [];
|
|
3550
3505
|
this.streamState = new Map();
|
|
3551
|
-
this.
|
|
3506
|
+
this.activityLogs = new Map();
|
|
3552
3507
|
setInterval(() => {
|
|
3553
3508
|
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
3554
|
-
for (const [runId,
|
|
3555
|
-
if (
|
|
3509
|
+
for (const [runId, log] of this.activityLogs) {
|
|
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
|
+
}
|
|
3556
3523
|
}
|
|
3557
3524
|
}, 5 * 60 * 1000);
|
|
3558
3525
|
}
|
|
@@ -3601,11 +3568,11 @@ export function createApp(config = {}) {
|
|
|
3601
3568
|
return;
|
|
3602
3569
|
}
|
|
3603
3570
|
if (state === 'final' || state === 'aborted' || state === 'error') this.streamState.delete(sessionKey);
|
|
3604
|
-
if (state === 'final')
|
|
3571
|
+
if (state === 'final') this.saveAssistantMessage(sessionKey, message, seq);
|
|
3605
3572
|
if (state === 'error') this.saveErrorMarker(sessionKey, message);
|
|
3606
3573
|
}
|
|
3607
3574
|
|
|
3608
|
-
saveAssistantMessage(sessionKey, message, seq
|
|
3575
|
+
saveAssistantMessage(sessionKey, message, seq) {
|
|
3609
3576
|
const parsed = parseSessionKey(sessionKey);
|
|
3610
3577
|
if (!parsed) return;
|
|
3611
3578
|
const ws = _getWorkspaces();
|
|
@@ -3616,12 +3583,37 @@ export function createApp(config = {}) {
|
|
|
3616
3583
|
const content = extractContent(message);
|
|
3617
3584
|
if (!content || !content.trim()) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
3618
3585
|
const now = Date.now();
|
|
3619
|
-
|
|
3620
|
-
|
|
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}`;
|
|
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
|
+
|
|
3621
3615
|
try {
|
|
3622
|
-
db.prepare('INSERT INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at) VALUES (?, ?, \'assistant\', \'text\', ?, \'sent\', ?, ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, seq = excluded.seq, timestamp = excluded.timestamp').run(messageId, parsed.threadId, content, msgSeq, now, now);
|
|
3623
3616
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
3624
|
-
// Always mark as unread — browser sends read receipts to clear
|
|
3625
3617
|
db.prepare('INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)').run(parsed.threadId, messageId, now);
|
|
3626
3618
|
syncThreadUnreadCount(db, parsed.threadId);
|
|
3627
3619
|
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId);
|
|
@@ -3630,7 +3622,7 @@ export function createApp(config = {}) {
|
|
|
3630
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 }));
|
|
3631
3623
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
3632
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 }));
|
|
3633
|
-
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})`);
|
|
3634
3626
|
} catch (e) { console.error(`Failed to save assistant message:`, e.message); }
|
|
3635
3627
|
}
|
|
3636
3628
|
|
|
@@ -3655,114 +3647,121 @@ export function createApp(config = {}) {
|
|
|
3655
3647
|
handleAgentEvent(payload) {
|
|
3656
3648
|
const { runId, stream, data, sessionKey } = payload;
|
|
3657
3649
|
if (!runId) return;
|
|
3658
|
-
if (!this.
|
|
3659
|
-
const
|
|
3660
|
-
const parsed = parseSessionKey(sessionKey);
|
|
3661
|
-
if (!parsed) return;
|
|
3662
|
-
const wsData = _getWorkspaces();
|
|
3663
|
-
if (!wsData.workspaces[parsed.workspace]) return;
|
|
3664
|
-
const db = _getDb(parsed.workspace);
|
|
3665
|
-
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
3666
|
-
if (!thread) return;
|
|
3667
|
-
|
|
3650
|
+
if (!this.activityLogs.has(runId)) this.activityLogs.set(runId, { sessionKey, steps: [], startTime: Date.now() });
|
|
3651
|
+
const log = this.activityLogs.get(runId);
|
|
3668
3652
|
if (stream === 'assistant') {
|
|
3669
3653
|
const text = data?.text || '';
|
|
3670
|
-
if (
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
} else { run.currentTextContent = text; }
|
|
3678
|
-
const now = Date.now();
|
|
3679
|
-
if (!run.lastTextBroadcast || now - run.lastTextBroadcast >= 150) {
|
|
3680
|
-
run.lastTextBroadcast = now;
|
|
3681
|
-
this.broadcastSegmentUpdate(parsed, runId, { id: run.currentTextSegmentId, type: 'text', seq: run.currentTextSeq, content: run.currentTextContent });
|
|
3654
|
+
if (text) {
|
|
3655
|
+
let currentSegment = log._currentAssistantSegment;
|
|
3656
|
+
if (!currentSegment || currentSegment._sealed) {
|
|
3657
|
+
currentSegment = { type: 'assistant', timestamp: Date.now(), text, _sealed: false };
|
|
3658
|
+
log._currentAssistantSegment = currentSegment;
|
|
3659
|
+
log.steps.push(currentSegment);
|
|
3660
|
+
} else { currentSegment.text = text; }
|
|
3682
3661
|
}
|
|
3683
3662
|
return;
|
|
3684
3663
|
}
|
|
3685
|
-
|
|
3686
3664
|
if (stream === 'thinking') {
|
|
3687
3665
|
const thinkingText = data?.text || '';
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
run.thinkingSeq = run.seqCounter;
|
|
3693
|
-
const now = Date.now();
|
|
3694
|
-
try { db.prepare('INSERT INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at) VALUES (?, ?, \'assistant\', \'thinking\', ?, \'sent\', ?, ?, ?)').run(segId, parsed.threadId, thinkingText, run.seqCounter, now, now); } catch (e) { console.error('Failed to save thinking segment:', e.message); }
|
|
3695
|
-
} else {
|
|
3696
|
-
try { db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(thinkingText, run.thinkingSegmentId); } catch (e) { console.error('Failed to update thinking segment:', e.message); }
|
|
3697
|
-
}
|
|
3666
|
+
let thinkingStep = log.steps.find(s => s.type === 'thinking');
|
|
3667
|
+
if (thinkingStep) { thinkingStep.text = thinkingText; }
|
|
3668
|
+
else { log.steps.push({ type: 'thinking', timestamp: Date.now(), text: thinkingText }); }
|
|
3669
|
+
this._writeActivityToDb(runId, log);
|
|
3698
3670
|
const now = Date.now();
|
|
3699
|
-
if (!
|
|
3700
|
-
|
|
3701
|
-
this.
|
|
3671
|
+
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) {
|
|
3672
|
+
log._lastThinkingBroadcast = now;
|
|
3673
|
+
this._broadcastActivityUpdate(runId, log);
|
|
3702
3674
|
}
|
|
3703
|
-
return;
|
|
3704
3675
|
}
|
|
3705
|
-
|
|
3706
3676
|
if (stream === 'tool') {
|
|
3707
|
-
if (
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
if (
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
if (existingId) {
|
|
3720
|
-
const durationMs = run.toolStartTimes.get(toolCallId) ? Date.now() - run.toolStartTimes.get(toolCallId) : null;
|
|
3721
|
-
const metadata = { name: data?.name || 'unknown', status: 'done', meta: data?.meta || '', isError: data?.isError || false, durationMs };
|
|
3722
|
-
try { db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), existingId); } catch (e) { console.error('Failed to update tool segment result:', e.message); }
|
|
3723
|
-
this.broadcastSegmentUpdate(parsed, runId, { id: existingId, type: 'tool', seq: run.toolSeqMap.get(toolCallId), metadata });
|
|
3724
|
-
}
|
|
3725
|
-
} else if (phase === 'update') {
|
|
3726
|
-
const existingId = run.toolSegmentMap.get(toolCallId);
|
|
3727
|
-
if (existingId) {
|
|
3728
|
-
const metadata = { name: data?.name || 'unknown', status: 'running', meta: data?.meta || '' };
|
|
3729
|
-
try { db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), existingId); } catch (e) { console.error('Failed to update tool segment:', e.message); }
|
|
3730
|
-
this.broadcastSegmentUpdate(parsed, runId, { id: existingId, type: 'tool', seq: run.toolSeqMap.get(toolCallId), metadata });
|
|
3731
|
-
}
|
|
3732
|
-
} else {
|
|
3733
|
-
run.seqCounter++;
|
|
3734
|
-
const segId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
3735
|
-
const now = Date.now();
|
|
3736
|
-
const metadata = { name: data?.name || 'unknown', status: 'running', meta: data?.meta || '' };
|
|
3737
|
-
try { db.prepare('INSERT INTO messages (id, thread_id, role, type, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, \'assistant\', \'tool\', \'\', \'sent\', ?, ?, ?, ?)').run(segId, parsed.threadId, JSON.stringify(metadata), run.seqCounter, now, now); } catch (e) { console.error('Failed to save tool segment:', e.message); }
|
|
3738
|
-
run.toolSegmentMap.set(toolCallId, segId); run.toolSeqMap.set(toolCallId, run.seqCounter); run.toolStartTimes.set(toolCallId, now);
|
|
3739
|
-
this.broadcastSegmentUpdate(parsed, runId, { id: segId, type: 'tool', seq: run.seqCounter, metadata });
|
|
3740
|
-
}
|
|
3741
|
-
return;
|
|
3677
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) { log._currentAssistantSegment._sealed = true; }
|
|
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 };
|
|
3679
|
+
if (data?.phase === 'result') {
|
|
3680
|
+
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
|
|
3681
|
+
if (existing) { existing.phase = 'done'; existing.resultMeta = data?.meta; existing.isError = data?.isError || false; existing.durationMs = Date.now() - existing.timestamp; }
|
|
3682
|
+
else { step.phase = 'done'; log.steps.push(step); }
|
|
3683
|
+
} else if (data?.phase === 'update') {
|
|
3684
|
+
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId);
|
|
3685
|
+
if (existing) { if (data?.meta) existing.resultMeta = data.meta; if (data?.isError) existing.isError = true; existing.phase = 'running'; }
|
|
3686
|
+
} else { log.steps.push(step); }
|
|
3687
|
+
this._writeActivityToDb(runId, log);
|
|
3688
|
+
this._broadcastActivityUpdate(runId, log);
|
|
3742
3689
|
}
|
|
3743
|
-
|
|
3744
3690
|
if (stream === 'lifecycle') {
|
|
3745
3691
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
3746
|
-
if (
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
setTimeout(() => this.runState.delete(runId), 30000);
|
|
3692
|
+
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
|
|
3693
|
+
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
3694
|
+
if (lastAssistantIdx >= 0) log.steps.splice(lastAssistantIdx, 1);
|
|
3695
|
+
this._writeActivityToDb(runId, log);
|
|
3696
|
+
this.activityLogs.delete(runId);
|
|
3697
|
+
return;
|
|
3753
3698
|
}
|
|
3754
3699
|
}
|
|
3755
3700
|
}
|
|
3756
3701
|
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3702
|
+
_writeActivityToDb(runId, log) {
|
|
3703
|
+
if (!log._parsed) {
|
|
3704
|
+
log._parsed = parseSessionKey(log.sessionKey);
|
|
3705
|
+
}
|
|
3706
|
+
const parsed = log._parsed;
|
|
3707
|
+
if (!parsed) return;
|
|
3708
|
+
|
|
3709
|
+
const db = _getDb(parsed.workspace);
|
|
3710
|
+
if (!db) return;
|
|
3711
|
+
|
|
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);
|
|
3760
3747
|
}
|
|
3761
|
-
return null;
|
|
3762
3748
|
}
|
|
3763
3749
|
|
|
3764
|
-
|
|
3765
|
-
|
|
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
|
+
}));
|
|
3766
3765
|
}
|
|
3767
3766
|
|
|
3768
3767
|
generateActivitySummary(steps) {
|