@clawchatsai/connector 0.0.28 → 0.0.29
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 +319 -386
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -368,6 +368,13 @@ 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
|
+
|
|
371
378
|
// FTS5 table — CREATE VIRTUAL TABLE doesn't support IF NOT EXISTS in all versions,
|
|
372
379
|
// so check if it exists first
|
|
373
380
|
const hasFts = db.prepare(
|
|
@@ -832,12 +839,14 @@ function handleGetThreads(req, res, params, query) {
|
|
|
832
839
|
`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
|
|
833
840
|
).get(...matchingIds).c;
|
|
834
841
|
threads = db.prepare(
|
|
835
|
-
`SELECT
|
|
842
|
+
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
843
|
+
FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
836
844
|
).all(...matchingIds, limit, offset);
|
|
837
845
|
} else {
|
|
838
846
|
total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
|
|
839
847
|
threads = db.prepare(
|
|
840
|
-
|
|
848
|
+
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
849
|
+
FROM threads t ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
841
850
|
).all(limit, offset);
|
|
842
851
|
}
|
|
843
852
|
|
|
@@ -2224,14 +2233,14 @@ class GatewayClient {
|
|
|
2224
2233
|
this.maxReconnectDelay = 30000;
|
|
2225
2234
|
this.browserClients = new Map(); // Map<WebSocket, { activeWorkspace, activeThreadId }>
|
|
2226
2235
|
this.streamState = new Map(); // Map<sessionKey, { state, buffer, threadId }>
|
|
2227
|
-
this.
|
|
2236
|
+
this.runState = new Map(); // Map<runId, { sessionKey, seqCounter, currentTextSegmentId, startTime }>
|
|
2228
2237
|
|
|
2229
|
-
// Clean up stale
|
|
2238
|
+
// Clean up stale run states every 5 minutes (runs that never completed)
|
|
2230
2239
|
setInterval(() => {
|
|
2231
2240
|
const cutoff = Date.now() - 10 * 60 * 1000; // 10 minutes
|
|
2232
|
-
for (const [runId,
|
|
2233
|
-
if (
|
|
2234
|
-
this.
|
|
2241
|
+
for (const [runId, run] of this.runState) {
|
|
2242
|
+
if (run.startTime < cutoff) {
|
|
2243
|
+
this.runState.delete(runId);
|
|
2235
2244
|
}
|
|
2236
2245
|
}
|
|
2237
2246
|
}, 5 * 60 * 1000);
|
|
@@ -2311,17 +2320,6 @@ class GatewayClient {
|
|
|
2311
2320
|
this.broadcastGatewayStatus(true);
|
|
2312
2321
|
}
|
|
2313
2322
|
|
|
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
2323
|
// Forward all messages to browser clients
|
|
2326
2324
|
this.broadcastToBrowsers(data);
|
|
2327
2325
|
|
|
@@ -2356,9 +2354,10 @@ class GatewayClient {
|
|
|
2356
2354
|
this.streamState.delete(sessionKey);
|
|
2357
2355
|
}
|
|
2358
2356
|
|
|
2359
|
-
// Save assistant messages on final
|
|
2357
|
+
// Save assistant messages on final (use segment seq position if available)
|
|
2360
2358
|
if (state === 'final') {
|
|
2361
|
-
this.
|
|
2359
|
+
const run = this.findRunBySessionKey(sessionKey);
|
|
2360
|
+
this.saveAssistantMessage(sessionKey, message, seq, run?.finalSeq);
|
|
2362
2361
|
}
|
|
2363
2362
|
|
|
2364
2363
|
// Save error markers
|
|
@@ -2367,7 +2366,7 @@ class GatewayClient {
|
|
|
2367
2366
|
}
|
|
2368
2367
|
}
|
|
2369
2368
|
|
|
2370
|
-
saveAssistantMessage(sessionKey, message, seq) {
|
|
2369
|
+
saveAssistantMessage(sessionKey, message, seq, segmentSeq) {
|
|
2371
2370
|
const parsed = parseSessionKey(sessionKey);
|
|
2372
2371
|
if (!parsed) return; // Non-ClawChats session key, silently ignore
|
|
2373
2372
|
|
|
@@ -2400,13 +2399,16 @@ class GatewayClient {
|
|
|
2400
2399
|
const now = Date.now();
|
|
2401
2400
|
const messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
2402
2401
|
|
|
2402
|
+
// Use segment seq for correct ordering after tool segments
|
|
2403
|
+
const msgSeq = segmentSeq != null ? segmentSeq : (seq != null ? seq : null);
|
|
2404
|
+
|
|
2403
2405
|
// Upsert message: INSERT OR REPLACE (same seq → same messageId → update content)
|
|
2404
2406
|
try {
|
|
2405
2407
|
db.prepare(`
|
|
2406
|
-
INSERT INTO messages (id, thread_id, role, content, status, timestamp, created_at)
|
|
2407
|
-
VALUES (?, ?, 'assistant', ?, 'sent', ?, ?)
|
|
2408
|
-
ON CONFLICT(id) DO UPDATE SET content = excluded.content, timestamp = excluded.timestamp
|
|
2409
|
-
`).run(messageId, parsed.threadId, content, now, now);
|
|
2408
|
+
INSERT INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at)
|
|
2409
|
+
VALUES (?, ?, 'assistant', 'text', ?, 'sent', ?, ?, ?)
|
|
2410
|
+
ON CONFLICT(id) DO UPDATE SET content = excluded.content, seq = excluded.seq, timestamp = excluded.timestamp
|
|
2411
|
+
`).run(messageId, parsed.threadId, content, msgSeq, now, now);
|
|
2410
2412
|
|
|
2411
2413
|
// Update thread updated_at
|
|
2412
2414
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
@@ -2488,295 +2490,245 @@ class GatewayClient {
|
|
|
2488
2490
|
const { runId, stream, data, sessionKey } = payload;
|
|
2489
2491
|
if (!runId) return;
|
|
2490
2492
|
|
|
2491
|
-
// Initialize
|
|
2492
|
-
if (!this.
|
|
2493
|
-
this.
|
|
2493
|
+
// Initialize run state if needed
|
|
2494
|
+
if (!this.runState.has(runId)) {
|
|
2495
|
+
this.runState.set(runId, { sessionKey, seqCounter: 0, startTime: Date.now() });
|
|
2494
2496
|
}
|
|
2495
|
-
const
|
|
2497
|
+
const run = this.runState.get(runId);
|
|
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;
|
|
2496
2508
|
|
|
2497
2509
|
if (stream === 'assistant') {
|
|
2498
|
-
// Capture intermediate text turns (narration between tool calls)
|
|
2499
2510
|
const text = data?.text || '';
|
|
2500
|
-
if (text)
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2511
|
+
if (!text) return;
|
|
2512
|
+
|
|
2513
|
+
if (!run.currentTextSegmentId) {
|
|
2514
|
+
// Start a new text segment — broadcast only, saved when sealed or discarded on lifecycle.end
|
|
2515
|
+
run.seqCounter++;
|
|
2516
|
+
run.currentTextSegmentId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
2517
|
+
run.currentTextContent = text;
|
|
2518
|
+
run.currentTextSeq = run.seqCounter;
|
|
2519
|
+
run.currentTextTimestamp = Date.now();
|
|
2520
|
+
} else {
|
|
2521
|
+
// Accumulate text for this segment
|
|
2522
|
+
run.currentTextContent = text;
|
|
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
2535
|
}
|
|
2517
|
-
// Don't broadcast on every assistant delta — too noisy
|
|
2518
|
-
// We'll broadcast when a tool starts (which seals the current segment)
|
|
2519
2536
|
return;
|
|
2520
2537
|
}
|
|
2521
2538
|
|
|
2522
2539
|
if (stream === 'thinking') {
|
|
2523
|
-
// Reasoning/thinking text from the model (requires reasoningLevel: "stream")
|
|
2524
2540
|
const thinkingText = data?.text || '';
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2541
|
+
|
|
2542
|
+
if (!run.thinkingSegmentId) {
|
|
2543
|
+
run.seqCounter++;
|
|
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
|
+
}
|
|
2530
2556
|
} else {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
}
|
|
2557
|
+
try {
|
|
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
|
+
}
|
|
2536
2562
|
}
|
|
2537
|
-
|
|
2563
|
+
|
|
2564
|
+
// Broadcast (throttled ~300ms)
|
|
2538
2565
|
const now = Date.now();
|
|
2539
|
-
if (!
|
|
2540
|
-
|
|
2541
|
-
this.
|
|
2566
|
+
if (!run.lastThinkingBroadcast || now - run.lastThinkingBroadcast >= 300) {
|
|
2567
|
+
run.lastThinkingBroadcast = now;
|
|
2568
|
+
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2569
|
+
id: run.thinkingSegmentId,
|
|
2570
|
+
type: 'thinking',
|
|
2571
|
+
seq: run.thinkingSeq,
|
|
2572
|
+
content: thinkingText
|
|
2573
|
+
});
|
|
2542
2574
|
}
|
|
2575
|
+
return;
|
|
2543
2576
|
}
|
|
2544
2577
|
|
|
2545
2578
|
if (stream === 'tool') {
|
|
2546
|
-
// Seal any current
|
|
2547
|
-
if (
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2579
|
+
// Seal and save any current text segment (intermediate narration before this tool call)
|
|
2580
|
+
if (run.currentTextSegmentId && run.currentTextContent?.trim()) {
|
|
2581
|
+
const segId = run.currentTextSegmentId;
|
|
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
|
+
}
|
|
2551
2593
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2594
|
+
run.currentTextSegmentId = null;
|
|
2595
|
+
run.currentTextContent = null;
|
|
2596
|
+
run.currentTextSeq = null;
|
|
2597
|
+
|
|
2598
|
+
if (!run.toolSegmentMap) run.toolSegmentMap = new Map();
|
|
2599
|
+
if (!run.toolSeqMap) run.toolSeqMap = new Map();
|
|
2600
|
+
if (!run.toolStartTimes) run.toolStartTimes = new Map();
|
|
2601
|
+
|
|
2602
|
+
const toolCallId = data?.toolCallId;
|
|
2603
|
+
const phase = data?.phase || 'start';
|
|
2604
|
+
|
|
2605
|
+
if (phase === 'result') {
|
|
2606
|
+
const existingId = run.toolSegmentMap.get(toolCallId);
|
|
2607
|
+
if (existingId) {
|
|
2608
|
+
const durationMs = run.toolStartTimes.get(toolCallId) ? Date.now() - run.toolStartTimes.get(toolCallId) : null;
|
|
2609
|
+
const metadata = {
|
|
2610
|
+
name: data?.name || 'unknown',
|
|
2611
|
+
status: 'done',
|
|
2612
|
+
meta: data?.meta || '',
|
|
2613
|
+
isError: data?.isError || false,
|
|
2614
|
+
durationMs
|
|
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
|
+
});
|
|
2574
2627
|
}
|
|
2575
|
-
} else if (
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2628
|
+
} else if (phase === 'update') {
|
|
2629
|
+
const existingId = run.toolSegmentMap.get(toolCallId);
|
|
2630
|
+
if (existingId) {
|
|
2631
|
+
const metadata = {
|
|
2632
|
+
name: data?.name || 'unknown',
|
|
2633
|
+
status: 'running',
|
|
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
|
+
});
|
|
2583
2647
|
}
|
|
2584
|
-
// If no existing step found, silently ignore the orphaned update
|
|
2585
2648
|
} else {
|
|
2586
|
-
|
|
2649
|
+
// phase === 'start': insert new tool segment
|
|
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
|
+
});
|
|
2587
2675
|
}
|
|
2588
|
-
|
|
2589
|
-
// Forward to browser for real-time display
|
|
2590
|
-
this.broadcastActivityUpdate(runId, log);
|
|
2676
|
+
return;
|
|
2591
2677
|
}
|
|
2592
2678
|
|
|
2593
2679
|
if (stream === 'lifecycle') {
|
|
2594
2680
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
2595
|
-
//
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2681
|
+
// Broadcast final text content one last time for live display
|
|
2682
|
+
// Do NOT save to DB — handleChatEvent.final saves the authoritative final text
|
|
2683
|
+
if (run.currentTextSegmentId) {
|
|
2684
|
+
this.broadcastSegmentUpdate(parsed, runId, {
|
|
2685
|
+
id: run.currentTextSegmentId,
|
|
2686
|
+
type: 'text',
|
|
2687
|
+
seq: run.currentTextSeq,
|
|
2688
|
+
content: run.currentTextContent || ''
|
|
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;
|
|
2604
2697
|
}
|
|
2605
2698
|
|
|
2606
|
-
|
|
2699
|
+
// Broadcast segments-complete
|
|
2700
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
2701
|
+
type: 'clawchats',
|
|
2702
|
+
event: 'segments-complete',
|
|
2703
|
+
workspace: parsed.workspace,
|
|
2704
|
+
threadId: parsed.threadId,
|
|
2705
|
+
runId
|
|
2706
|
+
}));
|
|
2607
2707
|
|
|
2608
|
-
//
|
|
2609
|
-
this.
|
|
2708
|
+
// Keep run state briefly for handleChatEvent.final to read finalSeq
|
|
2709
|
+
setTimeout(() => this.runState.delete(runId), 30000);
|
|
2610
2710
|
}
|
|
2611
2711
|
}
|
|
2612
2712
|
}
|
|
2613
2713
|
|
|
2614
|
-
|
|
2615
|
-
const
|
|
2616
|
-
|
|
2714
|
+
findRunBySessionKey(sessionKey) {
|
|
2715
|
+
for (const [, run] of this.runState.entries()) {
|
|
2716
|
+
if (run.sessionKey === sessionKey) return run;
|
|
2717
|
+
}
|
|
2718
|
+
return null;
|
|
2719
|
+
}
|
|
2617
2720
|
|
|
2721
|
+
broadcastSegmentUpdate(parsed, runId, segment) {
|
|
2618
2722
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2619
2723
|
type: 'clawchats',
|
|
2620
|
-
event: '
|
|
2724
|
+
event: 'segment-update',
|
|
2621
2725
|
workspace: parsed.workspace,
|
|
2622
2726
|
threadId: parsed.threadId,
|
|
2623
2727
|
runId,
|
|
2624
|
-
|
|
2625
|
-
summary: this.generateActivitySummary(log.steps)
|
|
2728
|
+
segment
|
|
2626
2729
|
}));
|
|
2627
2730
|
}
|
|
2628
2731
|
|
|
2629
|
-
finalizeActivityLog(runId, log) {
|
|
2630
|
-
const parsed = log.sessionKey ? parseSessionKey(log.sessionKey) : null;
|
|
2631
|
-
if (!parsed) return;
|
|
2632
|
-
|
|
2633
|
-
const db = getDb(parsed.workspace);
|
|
2634
|
-
if (!db) return;
|
|
2635
|
-
|
|
2636
|
-
const hasToolSteps = log.steps.some(s => s.type === 'tool');
|
|
2637
|
-
|
|
2638
|
-
// If we got no tool events (e.g., webchat session where we're not a registered recipient),
|
|
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
|
-
}
|
|
2648
|
-
|
|
2649
|
-
fetchToolCallsFromHistory(sessionKey, parsed, db, log, runId) {
|
|
2650
|
-
const reqId = `activity-hist-${runId}`;
|
|
2651
|
-
|
|
2652
|
-
const timeout = setTimeout(() => {
|
|
2653
|
-
// Timeout: save what we have (even if empty)
|
|
2654
|
-
this._pendingActivityCallbacks?.delete(reqId);
|
|
2655
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
2656
|
-
}, 5000);
|
|
2657
|
-
|
|
2658
|
-
// Register callback for when the gateway responds
|
|
2659
|
-
if (!this._pendingActivityCallbacks) this._pendingActivityCallbacks = new Map();
|
|
2660
|
-
this._pendingActivityCallbacks.set(reqId, (payload) => {
|
|
2661
|
-
clearTimeout(timeout);
|
|
2662
|
-
|
|
2663
|
-
// Extract tool calls and narration from the last assistant message in history
|
|
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
|
-
}
|
|
2709
|
-
|
|
2710
|
-
this.saveActivityToMessage(parsed, db, log, runId);
|
|
2711
|
-
this.broadcastActivityUpdate(runId, log);
|
|
2712
|
-
});
|
|
2713
|
-
|
|
2714
|
-
// Request session history from gateway
|
|
2715
|
-
this.ws.send(JSON.stringify({
|
|
2716
|
-
type: 'req',
|
|
2717
|
-
id: reqId,
|
|
2718
|
-
method: 'sessions.history',
|
|
2719
|
-
params: { key: sessionKey, limit: 3, includeTools: true }
|
|
2720
|
-
}));
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
extractToolMeta(toolUseBlock) {
|
|
2724
|
-
if (!toolUseBlock.input) return '';
|
|
2725
|
-
const input = toolUseBlock.input;
|
|
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
|
-
}
|
|
2734
|
-
|
|
2735
|
-
saveActivityToMessage(parsed, db, log, runId) {
|
|
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
|
-
}
|
|
2775
|
-
|
|
2776
|
-
this.activityLogs.delete(runId);
|
|
2777
|
-
}, 1000); // 1s delay — must happen after message-saved (which fires on chat:final)
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
2732
|
generateActivitySummary(steps) {
|
|
2781
2733
|
const toolSteps = steps.filter(s => s.type === 'tool' && s.phase !== 'result' && s.phase !== 'update');
|
|
2782
2734
|
const hasThinking = steps.some(s => s.type === 'thinking' && s.text);
|
|
@@ -3596,11 +3548,11 @@ export function createApp(config = {}) {
|
|
|
3596
3548
|
this.browserClients = new Map();
|
|
3597
3549
|
this._externalBroadcastTargets = [];
|
|
3598
3550
|
this.streamState = new Map();
|
|
3599
|
-
this.
|
|
3551
|
+
this.runState = new Map(); // Map<runId, { sessionKey, seqCounter, currentTextSegmentId, startTime }>
|
|
3600
3552
|
setInterval(() => {
|
|
3601
3553
|
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
3602
|
-
for (const [runId,
|
|
3603
|
-
if (
|
|
3554
|
+
for (const [runId, run] of this.runState) {
|
|
3555
|
+
if (run.startTime < cutoff) this.runState.delete(runId);
|
|
3604
3556
|
}
|
|
3605
3557
|
}, 5 * 60 * 1000);
|
|
3606
3558
|
}
|
|
@@ -3632,11 +3584,6 @@ export function createApp(config = {}) {
|
|
|
3632
3584
|
return;
|
|
3633
3585
|
}
|
|
3634
3586
|
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
3587
|
this.broadcastToBrowsers(data);
|
|
3641
3588
|
if (msg.type === 'event' && msg.event === 'chat' && msg.payload) this.handleChatEvent(msg.payload);
|
|
3642
3589
|
if (msg.type === 'event' && msg.event === 'agent' && msg.payload) this.handleAgentEvent(msg.payload);
|
|
@@ -3654,11 +3601,11 @@ export function createApp(config = {}) {
|
|
|
3654
3601
|
return;
|
|
3655
3602
|
}
|
|
3656
3603
|
if (state === 'final' || state === 'aborted' || state === 'error') this.streamState.delete(sessionKey);
|
|
3657
|
-
if (state === 'final') this.saveAssistantMessage(sessionKey, message, seq);
|
|
3604
|
+
if (state === 'final') { const run = this.findRunBySessionKey(sessionKey); this.saveAssistantMessage(sessionKey, message, seq, run?.finalSeq); }
|
|
3658
3605
|
if (state === 'error') this.saveErrorMarker(sessionKey, message);
|
|
3659
3606
|
}
|
|
3660
3607
|
|
|
3661
|
-
saveAssistantMessage(sessionKey, message, seq) {
|
|
3608
|
+
saveAssistantMessage(sessionKey, message, seq, segmentSeq) {
|
|
3662
3609
|
const parsed = parseSessionKey(sessionKey);
|
|
3663
3610
|
if (!parsed) return;
|
|
3664
3611
|
const ws = _getWorkspaces();
|
|
@@ -3670,8 +3617,9 @@ export function createApp(config = {}) {
|
|
|
3670
3617
|
if (!content || !content.trim()) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
3671
3618
|
const now = Date.now();
|
|
3672
3619
|
const messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
3620
|
+
const msgSeq = segmentSeq != null ? segmentSeq : (seq != null ? seq : null);
|
|
3673
3621
|
try {
|
|
3674
|
-
db.prepare(
|
|
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);
|
|
3675
3623
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(now, parsed.threadId);
|
|
3676
3624
|
// Always mark as unread — browser sends read receipts to clear
|
|
3677
3625
|
db.prepare('INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)').run(parsed.threadId, messageId, now);
|
|
@@ -3707,129 +3655,114 @@ export function createApp(config = {}) {
|
|
|
3707
3655
|
handleAgentEvent(payload) {
|
|
3708
3656
|
const { runId, stream, data, sessionKey } = payload;
|
|
3709
3657
|
if (!runId) return;
|
|
3710
|
-
if (!this.
|
|
3711
|
-
const
|
|
3658
|
+
if (!this.runState.has(runId)) this.runState.set(runId, { sessionKey, seqCounter: 0, startTime: Date.now() });
|
|
3659
|
+
const run = this.runState.get(runId);
|
|
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
|
+
|
|
3712
3668
|
if (stream === 'assistant') {
|
|
3713
3669
|
const text = data?.text || '';
|
|
3714
|
-
if (text)
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3670
|
+
if (!text) return;
|
|
3671
|
+
if (!run.currentTextSegmentId) {
|
|
3672
|
+
run.seqCounter++;
|
|
3673
|
+
run.currentTextSegmentId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
3674
|
+
run.currentTextContent = text;
|
|
3675
|
+
run.currentTextSeq = run.seqCounter;
|
|
3676
|
+
run.currentTextTimestamp = Date.now();
|
|
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 });
|
|
3721
3682
|
}
|
|
3722
3683
|
return;
|
|
3723
3684
|
}
|
|
3685
|
+
|
|
3724
3686
|
if (stream === 'thinking') {
|
|
3725
3687
|
const thinkingText = data?.text || '';
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3688
|
+
if (!run.thinkingSegmentId) {
|
|
3689
|
+
run.seqCounter++;
|
|
3690
|
+
const segId = `seg-${parsed.threadId}-${runId}-${run.seqCounter}`;
|
|
3691
|
+
run.thinkingSegmentId = segId;
|
|
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
|
+
}
|
|
3729
3698
|
const now = Date.now();
|
|
3730
|
-
if (!
|
|
3731
|
-
|
|
3732
|
-
this.
|
|
3699
|
+
if (!run.lastThinkingBroadcast || now - run.lastThinkingBroadcast >= 300) {
|
|
3700
|
+
run.lastThinkingBroadcast = now;
|
|
3701
|
+
this.broadcastSegmentUpdate(parsed, runId, { id: run.thinkingSegmentId, type: 'thinking', seq: run.thinkingSeq, content: thinkingText });
|
|
3733
3702
|
}
|
|
3703
|
+
return;
|
|
3734
3704
|
}
|
|
3705
|
+
|
|
3735
3706
|
if (stream === 'tool') {
|
|
3736
|
-
if (
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3707
|
+
if (run.currentTextSegmentId && run.currentTextContent?.trim()) {
|
|
3708
|
+
const { currentTextSegmentId: segId, currentTextContent: content, currentTextSeq: seq, currentTextTimestamp: ts = Date.now() } = run;
|
|
3709
|
+
try { db.prepare('INSERT OR REPLACE INTO messages (id, thread_id, role, type, content, status, seq, timestamp, created_at) VALUES (?, ?, \'assistant\', \'text\', ?, \'sent\', ?, ?, ?)').run(segId, parsed.threadId, content, seq, ts, ts); } catch (e) { console.error('Failed to save sealed text segment:', e.message); }
|
|
3710
|
+
}
|
|
3711
|
+
run.currentTextSegmentId = null; run.currentTextContent = null; run.currentTextSeq = null;
|
|
3712
|
+
if (!run.toolSegmentMap) run.toolSegmentMap = new Map();
|
|
3713
|
+
if (!run.toolSeqMap) run.toolSeqMap = new Map();
|
|
3714
|
+
if (!run.toolStartTimes) run.toolStartTimes = new Map();
|
|
3715
|
+
const toolCallId = data?.toolCallId;
|
|
3716
|
+
const phase = data?.phase || 'start';
|
|
3717
|
+
if (phase === 'result') {
|
|
3718
|
+
const existingId = run.toolSegmentMap.get(toolCallId);
|
|
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;
|
|
3747
3742
|
}
|
|
3743
|
+
|
|
3748
3744
|
if (stream === 'lifecycle') {
|
|
3749
3745
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
3750
|
-
if (
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3746
|
+
if (run.currentTextSegmentId) {
|
|
3747
|
+
this.broadcastSegmentUpdate(parsed, runId, { id: run.currentTextSegmentId, type: 'text', seq: run.currentTextSeq, content: run.currentTextContent || '' });
|
|
3748
|
+
run.finalSeq = run.currentTextSeq;
|
|
3749
|
+
run.currentTextSegmentId = null; run.currentTextContent = null;
|
|
3750
|
+
} else { run.finalSeq = run.seqCounter + 1; }
|
|
3751
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'segments-complete', workspace: parsed.workspace, threadId: parsed.threadId, runId }));
|
|
3752
|
+
setTimeout(() => this.runState.delete(runId), 30000);
|
|
3755
3753
|
}
|
|
3756
3754
|
}
|
|
3757
3755
|
}
|
|
3758
3756
|
|
|
3759
|
-
|
|
3760
|
-
const
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
finalizeActivityLog(runId, log) {
|
|
3766
|
-
const parsed = log.sessionKey ? parseSessionKey(log.sessionKey) : null;
|
|
3767
|
-
if (!parsed) return;
|
|
3768
|
-
const db = _getDb(parsed.workspace);
|
|
3769
|
-
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
|
-
|
|
3802
|
-
extractToolMeta(toolUseBlock) {
|
|
3803
|
-
if (!toolUseBlock.input) return '';
|
|
3804
|
-
const input = toolUseBlock.input;
|
|
3805
|
-
if (input.query) return input.query;
|
|
3806
|
-
if (input.command) return input.command.substring(0, 80);
|
|
3807
|
-
if (input.file_path || input.path) return input.file_path || input.path;
|
|
3808
|
-
if (input.url) return input.url;
|
|
3809
|
-
if (input.text) return input.text.substring(0, 60);
|
|
3810
|
-
return '';
|
|
3757
|
+
findRunBySessionKey(sessionKey) {
|
|
3758
|
+
for (const [, run] of this.runState.entries()) {
|
|
3759
|
+
if (run.sessionKey === sessionKey) return run;
|
|
3760
|
+
}
|
|
3761
|
+
return null;
|
|
3811
3762
|
}
|
|
3812
3763
|
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
try {
|
|
3816
|
-
const msg = db.prepare('SELECT id, metadata FROM messages WHERE thread_id = ? AND role = ? ORDER BY timestamp DESC LIMIT 1').get(parsed.threadId, 'assistant');
|
|
3817
|
-
if (msg) {
|
|
3818
|
-
const toolSteps = log.steps.filter(s => s.type === 'tool' && s.phase !== 'update');
|
|
3819
|
-
const thinkingSteps = log.steps.filter(s => s.type === 'thinking');
|
|
3820
|
-
const narrativeSteps = log.steps.filter(s => s.type === 'assistant' && s.text?.trim());
|
|
3821
|
-
if (toolSteps.length > 0 || thinkingSteps.length > 0 || narrativeSteps.length > 0) {
|
|
3822
|
-
const metadata = msg.metadata ? JSON.parse(msg.metadata) : {};
|
|
3823
|
-
metadata.activityLog = log.steps.map(s => { const clean = { ...s }; delete clean._sealed; return clean; });
|
|
3824
|
-
metadata.activitySummary = this.generateActivitySummary(log.steps);
|
|
3825
|
-
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), msg.id);
|
|
3826
|
-
console.log(`[ActivityLog] Saved ${toolSteps.length} tool steps for message ${msg.id}`);
|
|
3827
|
-
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'activity-saved', workspace: parsed.workspace, threadId: parsed.threadId, messageId: msg.id }));
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
} catch (e) { console.error('Failed to save activity log:', e.message); }
|
|
3831
|
-
this.activityLogs.delete(runId);
|
|
3832
|
-
}, 1000);
|
|
3764
|
+
broadcastSegmentUpdate(parsed, runId, segment) {
|
|
3765
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'segment-update', workspace: parsed.workspace, threadId: parsed.threadId, runId, segment }));
|
|
3833
3766
|
}
|
|
3834
3767
|
|
|
3835
3768
|
generateActivitySummary(steps) {
|