@clawchatsai/connector 0.0.36 → 0.0.37
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 +272 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1064,6 +1064,26 @@ async function handleCreateMessage(req, res, params) {
|
|
|
1064
1064
|
|
|
1065
1065
|
// Bump thread updated_at
|
|
1066
1066
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(Date.now(), params.id);
|
|
1067
|
+
|
|
1068
|
+
// Heuristic title on first user message (mimics Claude/ChatGPT — title appears immediately on send)
|
|
1069
|
+
if (body.role === 'user' && body.content) {
|
|
1070
|
+
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(params.id);
|
|
1071
|
+
if (threadInfo?.title === 'New chat') {
|
|
1072
|
+
const heuristic = body.content.replace(/\n.*/s, '').slice(0, 40).trim()
|
|
1073
|
+
+ (body.content.length > 40 ? '...' : '');
|
|
1074
|
+
if (heuristic) {
|
|
1075
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(heuristic, params.id);
|
|
1076
|
+
const activeWs = getWorkspaces().active;
|
|
1077
|
+
gatewayClient.broadcastToBrowsers(JSON.stringify({
|
|
1078
|
+
type: 'clawchats',
|
|
1079
|
+
event: 'thread-title-updated',
|
|
1080
|
+
threadId: params.id,
|
|
1081
|
+
workspace: activeWs,
|
|
1082
|
+
title: heuristic
|
|
1083
|
+
}));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1067
1087
|
}
|
|
1068
1088
|
|
|
1069
1089
|
const message = db.prepare('SELECT * FROM messages WHERE id = ?').get(body.id);
|
|
@@ -2297,6 +2317,15 @@ async function handleRequest(req, res) {
|
|
|
2297
2317
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/context-fill'))) {
|
|
2298
2318
|
return handleContextFill(req, res, p);
|
|
2299
2319
|
}
|
|
2320
|
+
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/generate-title'))) {
|
|
2321
|
+
const db = getActiveDb();
|
|
2322
|
+
const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(p.id);
|
|
2323
|
+
if (!thread) return sendError(res, 404, 'Thread not found');
|
|
2324
|
+
// Regenerate: sets heuristic immediately (safe fallback), then fires AI upgrade
|
|
2325
|
+
const activeWs = getWorkspaces().active;
|
|
2326
|
+
gatewayClient.generateThreadTitle(db, p.id, activeWs);
|
|
2327
|
+
return send(res, 200, { ok: true });
|
|
2328
|
+
}
|
|
2300
2329
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/upload'))) {
|
|
2301
2330
|
return await handleUpload(req, res, p);
|
|
2302
2331
|
}
|
|
@@ -2487,6 +2516,22 @@ class GatewayClient {
|
|
|
2487
2516
|
this.streamState.delete(sessionKey);
|
|
2488
2517
|
}
|
|
2489
2518
|
|
|
2519
|
+
// Intercept title generation responses (final, error, or aborted)
|
|
2520
|
+
if (sessionKey && sessionKey.includes('__clawchats_title_')) {
|
|
2521
|
+
if (state === 'final') {
|
|
2522
|
+
const content = extractContent(message);
|
|
2523
|
+
if (content && this.handleTitleResponse(sessionKey, content)) return;
|
|
2524
|
+
} else if (state === 'error' || state === 'aborted') {
|
|
2525
|
+
// Clean up pending entry by substring match — heuristic title stays
|
|
2526
|
+
if (this._pendingTitleGens) {
|
|
2527
|
+
for (const key of this._pendingTitleGens.keys()) {
|
|
2528
|
+
if (sessionKey === key || sessionKey.includes(key)) { this._pendingTitleGens.delete(key); break; }
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2490
2535
|
// Save assistant messages on final
|
|
2491
2536
|
if (state === 'final') {
|
|
2492
2537
|
this.saveAssistantMessage(sessionKey, message, seq);
|
|
@@ -2598,6 +2643,16 @@ class GatewayClient {
|
|
|
2598
2643
|
}));
|
|
2599
2644
|
|
|
2600
2645
|
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (${pendingMsg ? 'merged into pending' : 'seq: ' + seq})`);
|
|
2646
|
+
|
|
2647
|
+
// Auto-generate AI title upgrade after first assistant response
|
|
2648
|
+
// Heuristic was already set on user message save; this fires the AI upgrade
|
|
2649
|
+
const currentTitle = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId)?.title;
|
|
2650
|
+
if (currentTitle && currentTitle !== 'New chat') {
|
|
2651
|
+
const msgCount = db.prepare('SELECT COUNT(*) as c FROM messages WHERE thread_id = ?').get(parsed.threadId).c;
|
|
2652
|
+
if (msgCount >= 2) {
|
|
2653
|
+
this.generateThreadTitle(db, parsed.threadId, parsed.workspace, true);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2601
2656
|
} catch (e) {
|
|
2602
2657
|
console.error(`Failed to save assistant message:`, e.message);
|
|
2603
2658
|
}
|
|
@@ -2630,6 +2685,125 @@ class GatewayClient {
|
|
|
2630
2685
|
}
|
|
2631
2686
|
}
|
|
2632
2687
|
|
|
2688
|
+
/**
|
|
2689
|
+
* Generate a title for a thread. Optionally sets a heuristic title immediately,
|
|
2690
|
+
* then fires an async AI title upgrade via the gateway.
|
|
2691
|
+
* @param {boolean} skipHeuristic - If true, skip heuristic step (already set on user message save)
|
|
2692
|
+
*/
|
|
2693
|
+
generateThreadTitle(db, threadId, workspace, skipHeuristic = false) {
|
|
2694
|
+
const thread = db.prepare('SELECT title FROM threads WHERE id = ?').get(threadId);
|
|
2695
|
+
if (!thread) return;
|
|
2696
|
+
|
|
2697
|
+
// Skip if AI title gen already in progress for this thread
|
|
2698
|
+
const titleKey = `__clawchats_title_${threadId}`;
|
|
2699
|
+
if (this._pendingTitleGens?.has(titleKey)) return;
|
|
2700
|
+
|
|
2701
|
+
// Get first user message for heuristic title
|
|
2702
|
+
const firstUserMsg = db.prepare(
|
|
2703
|
+
"SELECT content FROM messages WHERE thread_id = ? AND role = 'user' ORDER BY created_at ASC LIMIT 1"
|
|
2704
|
+
).get(threadId);
|
|
2705
|
+
if (!firstUserMsg?.content) return;
|
|
2706
|
+
|
|
2707
|
+
// Step 1: Heuristic title (immediate) — skip if already set on user message save
|
|
2708
|
+
if (!skipHeuristic) {
|
|
2709
|
+
const heuristic = firstUserMsg.content.replace(/\n.*/s, '').slice(0, 40).trim()
|
|
2710
|
+
+ (firstUserMsg.content.length > 40 ? '...' : '');
|
|
2711
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(heuristic, threadId);
|
|
2712
|
+
|
|
2713
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
2714
|
+
type: 'clawchats',
|
|
2715
|
+
event: 'thread-title-updated',
|
|
2716
|
+
threadId,
|
|
2717
|
+
workspace,
|
|
2718
|
+
title: heuristic
|
|
2719
|
+
}));
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Step 2: AI title upgrade (async, best-effort)
|
|
2723
|
+
const messages = db.prepare(
|
|
2724
|
+
'SELECT role, content FROM messages WHERE thread_id = ? ORDER BY created_at ASC LIMIT 6'
|
|
2725
|
+
).all(threadId);
|
|
2726
|
+
|
|
2727
|
+
// Need at least 2 messages (user + assistant) for meaningful AI title
|
|
2728
|
+
if (messages.length < 2) return;
|
|
2729
|
+
|
|
2730
|
+
const conversation = messages.map(m => {
|
|
2731
|
+
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
2732
|
+
const content = m.content.length > 300 ? m.content.slice(0, 300) + '...' : m.content;
|
|
2733
|
+
return `${role}: ${content}`;
|
|
2734
|
+
}).join('\n\n');
|
|
2735
|
+
|
|
2736
|
+
const prompt = `Based on this conversation, generate a concise 3-5 word title. Return ONLY the title text, no quotes, no explanation:\n\n${conversation}\n\nTitle:`;
|
|
2737
|
+
|
|
2738
|
+
const reqId = `title-${threadId}-${Date.now()}`;
|
|
2739
|
+
if (!this._pendingTitleGens) this._pendingTitleGens = new Map();
|
|
2740
|
+
this._pendingTitleGens.set(titleKey, { threadId, workspace, reqId });
|
|
2741
|
+
|
|
2742
|
+
// Timeout cleanup — prevent unbounded map growth if gateway never responds
|
|
2743
|
+
setTimeout(() => {
|
|
2744
|
+
if (this._pendingTitleGens?.has(titleKey)) {
|
|
2745
|
+
this._pendingTitleGens.delete(titleKey);
|
|
2746
|
+
console.log(`Title gen timeout for ${threadId} — keeping heuristic title`);
|
|
2747
|
+
}
|
|
2748
|
+
}, 30000);
|
|
2749
|
+
|
|
2750
|
+
this.sendToGateway(JSON.stringify({
|
|
2751
|
+
type: 'req',
|
|
2752
|
+
id: reqId,
|
|
2753
|
+
method: 'chat.send',
|
|
2754
|
+
params: {
|
|
2755
|
+
sessionKey: titleKey,
|
|
2756
|
+
message: prompt,
|
|
2757
|
+
deliver: false,
|
|
2758
|
+
idempotencyKey: reqId
|
|
2759
|
+
}
|
|
2760
|
+
}));
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* Handle AI title response from gateway.
|
|
2765
|
+
* Returns true if the event was consumed (was a title gen response).
|
|
2766
|
+
*/
|
|
2767
|
+
handleTitleResponse(sessionKey, content) {
|
|
2768
|
+
if (!this._pendingTitleGens) return false;
|
|
2769
|
+
// Gateway may prefix sessionKey (e.g. agent:main:__clawchats_title_xxx)
|
|
2770
|
+
// Find the matching pending entry by substring
|
|
2771
|
+
let matchKey = null, pending = null;
|
|
2772
|
+
for (const [key, val] of this._pendingTitleGens) {
|
|
2773
|
+
if (sessionKey === key || sessionKey.includes(key)) {
|
|
2774
|
+
matchKey = key;
|
|
2775
|
+
pending = val;
|
|
2776
|
+
break;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (!pending) return false;
|
|
2780
|
+
|
|
2781
|
+
this._pendingTitleGens.delete(matchKey);
|
|
2782
|
+
|
|
2783
|
+
let title = content.trim()
|
|
2784
|
+
.replace(/^["']|["']$/g, '')
|
|
2785
|
+
.replace(/^Title:\s*/i, '')
|
|
2786
|
+
.replace(/\n.*/s, '')
|
|
2787
|
+
.trim();
|
|
2788
|
+
|
|
2789
|
+
if (title.length > 50) title = title.substring(0, 47) + '...';
|
|
2790
|
+
if (title.length === 0 || title.length >= 100) return true; // bad response, keep heuristic
|
|
2791
|
+
|
|
2792
|
+
const db = getDb(pending.workspace);
|
|
2793
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(title, pending.threadId);
|
|
2794
|
+
|
|
2795
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
2796
|
+
type: 'clawchats',
|
|
2797
|
+
event: 'thread-title-updated',
|
|
2798
|
+
threadId: pending.threadId,
|
|
2799
|
+
workspace: pending.workspace,
|
|
2800
|
+
title
|
|
2801
|
+
}));
|
|
2802
|
+
|
|
2803
|
+
console.log(`AI title generated for ${pending.threadId}: "${title}"`);
|
|
2804
|
+
return true;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2633
2807
|
handleAgentEvent(payload) {
|
|
2634
2808
|
const { runId, stream, data, sessionKey } = payload;
|
|
2635
2809
|
if (!runId) return;
|
|
@@ -3390,6 +3564,26 @@ export function createApp(config = {}) {
|
|
|
3390
3564
|
} else {
|
|
3391
3565
|
db.prepare('INSERT INTO messages (id, thread_id, role, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(body.id, params.id, body.role, body.content, body.status || 'sent', metadata, body.seq || null, body.timestamp, Date.now());
|
|
3392
3566
|
db.prepare('UPDATE threads SET updated_at = ? WHERE id = ?').run(Date.now(), params.id);
|
|
3567
|
+
|
|
3568
|
+
// Heuristic title on first user message (mimics Claude/ChatGPT — title appears immediately on send)
|
|
3569
|
+
if (body.role === 'user' && body.content) {
|
|
3570
|
+
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(params.id);
|
|
3571
|
+
if (threadInfo?.title === 'New chat') {
|
|
3572
|
+
const heuristic = body.content.replace(/\n.*/s, '').slice(0, 40).trim()
|
|
3573
|
+
+ (body.content.length > 40 ? '...' : '');
|
|
3574
|
+
if (heuristic) {
|
|
3575
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(heuristic, params.id);
|
|
3576
|
+
const activeWs = _getWorkspaces().active;
|
|
3577
|
+
_gatewayClient.broadcastToBrowsers(JSON.stringify({
|
|
3578
|
+
type: 'clawchats',
|
|
3579
|
+
event: 'thread-title-updated',
|
|
3580
|
+
threadId: params.id,
|
|
3581
|
+
workspace: activeWs,
|
|
3582
|
+
title: heuristic
|
|
3583
|
+
}));
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3393
3587
|
}
|
|
3394
3588
|
const message = db.prepare('SELECT * FROM messages WHERE id = ?').get(body.id);
|
|
3395
3589
|
if (message && message.metadata) { try { message.metadata = JSON.parse(message.metadata); } catch { /* ok */ } }
|
|
@@ -3703,6 +3897,22 @@ export function createApp(config = {}) {
|
|
|
3703
3897
|
return;
|
|
3704
3898
|
}
|
|
3705
3899
|
if (state === 'final' || state === 'aborted' || state === 'error') this.streamState.delete(sessionKey);
|
|
3900
|
+
|
|
3901
|
+
// Intercept title generation responses (final, error, or aborted)
|
|
3902
|
+
if (sessionKey && sessionKey.includes('__clawchats_title_')) {
|
|
3903
|
+
if (state === 'final') {
|
|
3904
|
+
const content = extractContent(message);
|
|
3905
|
+
if (content && this.handleTitleResponse(sessionKey, content)) return;
|
|
3906
|
+
} else if (state === 'error' || state === 'aborted') {
|
|
3907
|
+
if (this._pendingTitleGens) {
|
|
3908
|
+
for (const key of this._pendingTitleGens.keys()) {
|
|
3909
|
+
if (sessionKey === key || sessionKey.includes(key)) { this._pendingTitleGens.delete(key); break; }
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3706
3916
|
if (state === 'final') this.saveAssistantMessage(sessionKey, message, seq);
|
|
3707
3917
|
if (state === 'error') this.saveErrorMarker(sessionKey, message);
|
|
3708
3918
|
}
|
|
@@ -3758,6 +3968,15 @@ export function createApp(config = {}) {
|
|
|
3758
3968
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
3759
3969
|
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 }));
|
|
3760
3970
|
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (${pendingMsg ? 'merged into pending' : 'seq: ' + seq})`);
|
|
3971
|
+
|
|
3972
|
+
// Auto-generate AI title upgrade after first assistant response
|
|
3973
|
+
const currentTitle = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId)?.title;
|
|
3974
|
+
if (currentTitle && currentTitle !== 'New chat') {
|
|
3975
|
+
const msgCount = db.prepare('SELECT COUNT(*) as c FROM messages WHERE thread_id = ?').get(parsed.threadId).c;
|
|
3976
|
+
if (msgCount >= 2) {
|
|
3977
|
+
this.generateThreadTitle(db, parsed.threadId, parsed.workspace, true);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3761
3980
|
} catch (e) { console.error(`Failed to save assistant message:`, e.message); }
|
|
3762
3981
|
}
|
|
3763
3982
|
|
|
@@ -3779,6 +3998,50 @@ export function createApp(config = {}) {
|
|
|
3779
3998
|
} catch (e) { console.error(`Failed to save error marker:`, e.message); }
|
|
3780
3999
|
}
|
|
3781
4000
|
|
|
4001
|
+
generateThreadTitle(db, threadId, workspace, skipHeuristic = false) {
|
|
4002
|
+
const thread = db.prepare('SELECT title FROM threads WHERE id = ?').get(threadId);
|
|
4003
|
+
if (!thread) return;
|
|
4004
|
+
const titleKey = `__clawchats_title_${threadId}`;
|
|
4005
|
+
if (this._pendingTitleGens?.has(titleKey)) return;
|
|
4006
|
+
const firstUserMsg = db.prepare("SELECT content FROM messages WHERE thread_id = ? AND role = 'user' ORDER BY created_at ASC LIMIT 1").get(threadId);
|
|
4007
|
+
if (!firstUserMsg?.content) return;
|
|
4008
|
+
|
|
4009
|
+
if (!skipHeuristic) {
|
|
4010
|
+
const heuristic = firstUserMsg.content.replace(/\n.*/s, '').slice(0, 40).trim() + (firstUserMsg.content.length > 40 ? '...' : '');
|
|
4011
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(heuristic, threadId);
|
|
4012
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'thread-title-updated', threadId, workspace, title: heuristic }));
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
const messages = db.prepare('SELECT role, content FROM messages WHERE thread_id = ? ORDER BY created_at ASC LIMIT 6').all(threadId);
|
|
4016
|
+
if (messages.length < 2) return;
|
|
4017
|
+
const conversation = messages.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content.length > 300 ? m.content.slice(0, 300) + '...' : m.content}`).join('\n\n');
|
|
4018
|
+
const prompt = `Based on this conversation, generate a concise 3-5 word title. Return ONLY the title text, no quotes, no explanation:\n\n${conversation}\n\nTitle:`;
|
|
4019
|
+
const reqId = `title-${threadId}-${Date.now()}`;
|
|
4020
|
+
if (!this._pendingTitleGens) this._pendingTitleGens = new Map();
|
|
4021
|
+
this._pendingTitleGens.set(titleKey, { threadId, workspace, reqId });
|
|
4022
|
+
setTimeout(() => { if (this._pendingTitleGens?.has(titleKey)) { this._pendingTitleGens.delete(titleKey); console.log(`Title gen timeout for ${threadId} — keeping heuristic title`); } }, 30000);
|
|
4023
|
+
this.sendToGateway(JSON.stringify({ type: 'req', id: reqId, method: 'chat.send', params: { sessionKey: titleKey, message: prompt, deliver: false, idempotencyKey: reqId } }));
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
handleTitleResponse(sessionKey, content) {
|
|
4027
|
+
if (!this._pendingTitleGens) return false;
|
|
4028
|
+
// Gateway may prefix sessionKey (e.g. agent:main:__clawchats_title_xxx)
|
|
4029
|
+
let matchKey = null, pending = null;
|
|
4030
|
+
for (const [key, val] of this._pendingTitleGens) {
|
|
4031
|
+
if (sessionKey === key || sessionKey.includes(key)) { matchKey = key; pending = val; break; }
|
|
4032
|
+
}
|
|
4033
|
+
if (!pending) return false;
|
|
4034
|
+
this._pendingTitleGens.delete(matchKey);
|
|
4035
|
+
let title = content.trim().replace(/^["']|["']$/g, '').replace(/^Title:\s*/i, '').replace(/\n.*/s, '').trim();
|
|
4036
|
+
if (title.length > 50) title = title.substring(0, 47) + '...';
|
|
4037
|
+
if (title.length === 0 || title.length >= 100) return true;
|
|
4038
|
+
const db = _getDb(pending.workspace);
|
|
4039
|
+
db.prepare('UPDATE threads SET title = ? WHERE id = ?').run(title, pending.threadId);
|
|
4040
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'thread-title-updated', threadId: pending.threadId, workspace: pending.workspace, title }));
|
|
4041
|
+
console.log(`AI title generated for ${pending.threadId}: "${title}"`);
|
|
4042
|
+
return true;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
3782
4045
|
handleAgentEvent(payload) {
|
|
3783
4046
|
const { runId, stream, data, sessionKey } = payload;
|
|
3784
4047
|
if (!runId) return;
|
|
@@ -4122,6 +4385,15 @@ export function createApp(config = {}) {
|
|
|
4122
4385
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/messages'))) return await _handleCreateMessage(req, res, p);
|
|
4123
4386
|
if ((p = matchRoute(method, urlPath, 'DELETE /api/threads/:id/messages/:messageId'))) return _handleDeleteMessage(req, res, p);
|
|
4124
4387
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/context-fill'))) return _handleContextFill(req, res, p);
|
|
4388
|
+
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/generate-title'))) {
|
|
4389
|
+
const db = _getActiveDb();
|
|
4390
|
+
const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(p.id);
|
|
4391
|
+
if (!thread) return sendError(res, 404, 'Thread not found');
|
|
4392
|
+
// Regenerate: sets heuristic immediately (safe fallback), then fires AI upgrade
|
|
4393
|
+
const activeWs = _getWorkspaces().active;
|
|
4394
|
+
_gatewayClient.generateThreadTitle(db, p.id, activeWs);
|
|
4395
|
+
return send(res, 200, { ok: true });
|
|
4396
|
+
}
|
|
4125
4397
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/upload'))) return await _handleUpload(req, res, p);
|
|
4126
4398
|
if ((p = matchRoute(method, urlPath, 'GET /api/threads/:id/intelligence'))) return _handleGetIntelligence(req, res, p);
|
|
4127
4399
|
if ((p = matchRoute(method, urlPath, 'POST /api/threads/:id/intelligence'))) return await _handleSaveIntelligence(req, res, p);
|