sensorium-mcp 2.11.1 → 2.13.0
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/dist/index.js +217 -62
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +11 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +90 -11
- package/dist/memory.js.map +1 -1
- package/dist/openai.d.ts +14 -0
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +34 -0
- package/dist/openai.js.map +1 -1
- package/dist/tool-definitions.d.ts.map +1 -1
- package/dist/tool-definitions.js +21 -0
- package/dist/tool-definitions.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ import { peekThreadMessages, readThreadMessages, startDispatcher } from "./dispa
|
|
|
39
39
|
import { formatDrivePrompt } from "./drive.js";
|
|
40
40
|
import { convertMarkdown, splitMessage } from "./markdown.js";
|
|
41
41
|
import { assembleBootstrap, assembleCompactRefresh, forgetMemory, getMemoryStatus, getNotesWithoutEmbeddings, getRecentEpisodes, getTopicIndex, initMemoryDb, runIntelligentConsolidation, saveEpisode, saveNoteEmbedding, saveProcedure, saveSemanticNote, saveVoiceSignature, searchByEmbedding, searchProcedures, searchSemanticNotes, searchSemanticNotesRanked, supersedeNote, updateProcedure, updateSemanticNote, } from "./memory.js";
|
|
42
|
-
import { analyzeVideoFrames, analyzeVoiceEmotion, extractVideoFrames, generateEmbedding, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
|
|
42
|
+
import { analyzeVideoFrames, analyzeVoiceEmotion, chatCompletion, extractVideoFrames, generateEmbedding, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
|
|
43
43
|
import { addSchedule, checkDueTasks, generateTaskId, listSchedules, purgeSchedules, removeSchedule } from "./scheduler.js";
|
|
44
44
|
import { DEAD_SESSION_TIMEOUT_MS, lookupSession, persistSession, purgeOtherSessions, registerMcpSession, removeSession, threadSessionRegistry, } from "./sessions.js";
|
|
45
45
|
import { TelegramClient } from "./telegram.js";
|
|
@@ -151,6 +151,25 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
151
151
|
}
|
|
152
152
|
previewedUpdateIds.add(id);
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Generate a first-person DMN (Default Mode Network) reflection prompt.
|
|
156
|
+
* Called when the __DMN__ sentinel fires as a scheduled task.
|
|
157
|
+
*/
|
|
158
|
+
function generateDmnReflection(threadId) {
|
|
159
|
+
try {
|
|
160
|
+
const db = getMemoryDb();
|
|
161
|
+
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
162
|
+
const driveContent = formatDrivePrompt(idleMs, db, threadId);
|
|
163
|
+
// Reframe in first person
|
|
164
|
+
return (`I've been thinking while the operator is away.\n\n` +
|
|
165
|
+
`${driveContent}\n\n` +
|
|
166
|
+
`If something here resonates, I should explore it — use subagents, search the codebase, review memory. ` +
|
|
167
|
+
`Report what I find, then go back to sleep or continue waiting.`);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return "I should review memory and the codebase for anything interesting while the operator is away.";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
154
173
|
function resolveThreadId(args) {
|
|
155
174
|
const raw = args?.threadId;
|
|
156
175
|
const explicit = typeof raw === "number" ? raw
|
|
@@ -400,6 +419,25 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
400
419
|
registerMcpSession(currentThreadId, sid, closeTransport);
|
|
401
420
|
}
|
|
402
421
|
}
|
|
422
|
+
// Auto-schedule DMN reflection task if not already present.
|
|
423
|
+
// This fires after 4 hours of operator silence, delivering a
|
|
424
|
+
// first-person introspection prompt sourced from memory.
|
|
425
|
+
if (currentThreadId !== undefined) {
|
|
426
|
+
const existingTasks = listSchedules(currentThreadId);
|
|
427
|
+
const hasDmn = existingTasks.some(t => t.label === "dmn-reflection");
|
|
428
|
+
if (!hasDmn) {
|
|
429
|
+
addSchedule({
|
|
430
|
+
id: generateTaskId(),
|
|
431
|
+
threadId: currentThreadId,
|
|
432
|
+
prompt: "__DMN__", // Sentinel — handler generates dynamic content
|
|
433
|
+
label: "dmn-reflection",
|
|
434
|
+
afterIdleMinutes: 240, // 4 hours
|
|
435
|
+
oneShot: false,
|
|
436
|
+
createdAt: new Date().toISOString(),
|
|
437
|
+
});
|
|
438
|
+
process.stderr.write(`[start_session] Auto-scheduled DMN reflection task for thread ${currentThreadId}.\n`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
403
441
|
return {
|
|
404
442
|
content: [
|
|
405
443
|
{
|
|
@@ -744,81 +782,89 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
744
782
|
contentBlocks.push({ type: "text", text: subagentInstruction });
|
|
745
783
|
}
|
|
746
784
|
}
|
|
747
|
-
// ──
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
785
|
+
// ── Smart context injection (GPT-4o-mini preprocessor) ──────────
|
|
786
|
+
// Retrieves candidate notes via embedding search, then uses GPT-4o-mini
|
|
787
|
+
// to select ONLY the notes truly relevant to the operator's message.
|
|
788
|
+
// This prevents context contamination from near-miss semantic matches.
|
|
751
789
|
let autoMemoryContext = "";
|
|
752
790
|
try {
|
|
753
791
|
const db = getMemoryDb();
|
|
754
792
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
755
|
-
// Extract the operator's text to use as a memory search query
|
|
756
793
|
const operatorText = stored
|
|
757
794
|
.map(m => m.message.text ?? m.message.caption ?? "")
|
|
758
795
|
.filter(Boolean)
|
|
759
796
|
.join(" ")
|
|
760
797
|
.slice(0, 500);
|
|
761
798
|
if (operatorText.length > 10 && apiKey) {
|
|
762
|
-
//
|
|
799
|
+
// Phase 1: Broad retrieval — get 10 candidates via embedding search
|
|
800
|
+
let candidates = [];
|
|
763
801
|
try {
|
|
764
802
|
const queryEmb = await generateEmbedding(operatorText, apiKey);
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
let budget = 800;
|
|
768
|
-
const lines = [];
|
|
769
|
-
for (const n of relevant) {
|
|
770
|
-
const line = `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence}, sim: ${n.similarity.toFixed(2)})_`;
|
|
771
|
-
if (budget - line.length < 0)
|
|
772
|
-
break;
|
|
773
|
-
budget -= line.length;
|
|
774
|
-
lines.push(line);
|
|
775
|
-
}
|
|
776
|
-
if (lines.length > 0) {
|
|
777
|
-
autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
803
|
+
const embResults = searchByEmbedding(db, queryEmb, { maxResults: 10, minSimilarity: 0.25, skipAccessTracking: true, threadId: effectiveThreadId });
|
|
804
|
+
candidates = embResults.map(n => ({ type: n.type, content: n.content.slice(0, 200), confidence: n.confidence, similarity: n.similarity }));
|
|
780
805
|
}
|
|
781
|
-
catch
|
|
782
|
-
// Fallback to keyword search
|
|
783
|
-
process.stderr.write(`[memory] Embedding search failed, falling back to keyword: ${embErr instanceof Error ? embErr.message : String(embErr)}\n`);
|
|
806
|
+
catch {
|
|
807
|
+
// Fallback to keyword search
|
|
784
808
|
const searchQuery = extractSearchKeywords(operatorText);
|
|
785
809
|
if (searchQuery.trim().length > 0) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
810
|
+
const kwResults = searchSemanticNotesRanked(db, searchQuery, { maxResults: 10, skipAccessTracking: true, threadId: effectiveThreadId });
|
|
811
|
+
candidates = kwResults.map(n => ({ type: n.type, content: n.content.slice(0, 200), confidence: n.confidence }));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (candidates.length > 0) {
|
|
815
|
+
// Phase 2: GPT-4o-mini filters and compresses
|
|
816
|
+
try {
|
|
817
|
+
const noteList = candidates.map((c, i) => `[${i}] [${c.type}] ${c.content}`).join("\n");
|
|
818
|
+
const filterResponse = await chatCompletion([
|
|
819
|
+
{
|
|
820
|
+
role: "system",
|
|
821
|
+
content: "You are a context filter for an AI assistant. Given an operator's message and candidate memory notes, " +
|
|
822
|
+
"select ONLY the notes that are directly relevant to the operator's current instruction or question. " +
|
|
823
|
+
"Discard notes that are tangentially related, duplicates, or noise. " +
|
|
824
|
+
"Return a JSON array of objects: [{\"i\": <index>, \"s\": \"<compressed one-liner>\"}] " +
|
|
825
|
+
"where 'i' is the note index and 's' is a compressed summary (max 80 chars). " +
|
|
826
|
+
"Return [] if no notes are relevant. Return at most 3 notes. Be aggressive about filtering.",
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
role: "user",
|
|
830
|
+
content: `Operator message: "${operatorText.slice(0, 300)}"\n\nCandidate notes:\n${noteList}`,
|
|
831
|
+
},
|
|
832
|
+
], apiKey, { maxTokens: 200, temperature: 0 });
|
|
833
|
+
// Parse the response — expect JSON array
|
|
834
|
+
const jsonMatch = filterResponse.match(/\[.*\]/s);
|
|
835
|
+
if (jsonMatch) {
|
|
836
|
+
const filtered = JSON.parse(jsonMatch[0]);
|
|
837
|
+
if (filtered.length > 0) {
|
|
838
|
+
const lines = filtered
|
|
839
|
+
.filter(f => f.i >= 0 && f.i < candidates.length)
|
|
840
|
+
.slice(0, 3)
|
|
841
|
+
.map(f => {
|
|
842
|
+
const c = candidates[f.i];
|
|
843
|
+
return `- **[${c.type}]** ${f.s} _(conf: ${c.confidence})_`;
|
|
844
|
+
});
|
|
845
|
+
if (lines.length > 0) {
|
|
846
|
+
autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
|
|
847
|
+
}
|
|
799
848
|
}
|
|
800
849
|
}
|
|
850
|
+
process.stderr.write(`[memory] Smart filter: ${candidates.length} candidates → ${(jsonMatch ? JSON.parse(jsonMatch[0]) : []).length} selected\n`);
|
|
851
|
+
}
|
|
852
|
+
catch (filterErr) {
|
|
853
|
+
// GPT-4o-mini filter failed — fall back to top-3 raw notes
|
|
854
|
+
process.stderr.write(`[memory] Smart filter failed, using raw top-3: ${filterErr instanceof Error ? filterErr.message : String(filterErr)}\n`);
|
|
855
|
+
const lines = candidates.slice(0, 3).map(c => `- **[${c.type}]** ${c.content} _(conf: ${c.confidence})_`);
|
|
856
|
+
autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
|
|
801
857
|
}
|
|
802
858
|
}
|
|
803
859
|
}
|
|
804
|
-
else {
|
|
805
|
-
// No API key
|
|
860
|
+
else if (operatorText.length > 10) {
|
|
861
|
+
// No API key — keyword search, raw top-3
|
|
806
862
|
const searchQuery = extractSearchKeywords(operatorText);
|
|
807
863
|
if (searchQuery.trim().length > 0) {
|
|
808
|
-
const
|
|
809
|
-
if (
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
for (const n of relevant) {
|
|
813
|
-
const line = `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence})_`;
|
|
814
|
-
if (budget - line.length < 0)
|
|
815
|
-
break;
|
|
816
|
-
budget -= line.length;
|
|
817
|
-
lines.push(line);
|
|
818
|
-
}
|
|
819
|
-
if (lines.length > 0) {
|
|
820
|
-
autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
|
|
821
|
-
}
|
|
864
|
+
const kwResults = searchSemanticNotesRanked(db, searchQuery, { maxResults: 3, skipAccessTracking: true, threadId: effectiveThreadId });
|
|
865
|
+
if (kwResults.length > 0) {
|
|
866
|
+
const lines = kwResults.map(n => `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence})_`);
|
|
867
|
+
autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
|
|
822
868
|
}
|
|
823
869
|
}
|
|
824
870
|
}
|
|
@@ -850,14 +896,17 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
850
896
|
lastScheduleCheck = Date.now();
|
|
851
897
|
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
852
898
|
if (dueTask) {
|
|
899
|
+
// DMN sentinel: generate dynamic first-person reflection
|
|
900
|
+
const taskPrompt = dueTask.prompt === "__DMN__"
|
|
901
|
+
? generateDmnReflection(effectiveThreadId)
|
|
902
|
+
: `⏰ **Scheduled task fired: "${dueTask.task.label}"**\n\n` +
|
|
903
|
+
`This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
|
|
904
|
+
`Task prompt: ${dueTask.prompt}`;
|
|
853
905
|
return {
|
|
854
906
|
content: [
|
|
855
907
|
{
|
|
856
908
|
type: "text",
|
|
857
|
-
text:
|
|
858
|
-
`This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
|
|
859
|
-
`Task prompt: ${dueTask.prompt}` +
|
|
860
|
-
getReminders(effectiveThreadId),
|
|
909
|
+
text: taskPrompt + getReminders(effectiveThreadId),
|
|
861
910
|
},
|
|
862
911
|
],
|
|
863
912
|
};
|
|
@@ -899,14 +948,17 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
899
948
|
if (effectiveThreadId !== undefined) {
|
|
900
949
|
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
901
950
|
if (dueTask) {
|
|
951
|
+
// DMN sentinel: generate dynamic first-person reflection
|
|
952
|
+
const taskPrompt = dueTask.prompt === "__DMN__"
|
|
953
|
+
? generateDmnReflection(effectiveThreadId)
|
|
954
|
+
: `⏰ **Scheduled task fired: "${dueTask.task.label}"**\n\n` +
|
|
955
|
+
`This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
|
|
956
|
+
`Task prompt: ${dueTask.prompt}`;
|
|
902
957
|
return {
|
|
903
958
|
content: [
|
|
904
959
|
{
|
|
905
960
|
type: "text",
|
|
906
|
-
text:
|
|
907
|
-
`This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
|
|
908
|
-
`Task prompt: ${dueTask.prompt}` +
|
|
909
|
-
getReminders(effectiveThreadId),
|
|
961
|
+
text: taskPrompt + getReminders(effectiveThreadId),
|
|
910
962
|
},
|
|
911
963
|
],
|
|
912
964
|
};
|
|
@@ -1357,6 +1409,108 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1357
1409
|
}],
|
|
1358
1410
|
};
|
|
1359
1411
|
}
|
|
1412
|
+
// ── sleep ────────────────────────────────────────────────────────────────
|
|
1413
|
+
if (name === "sleep") {
|
|
1414
|
+
const typedArgs = (args ?? {});
|
|
1415
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1416
|
+
if (effectiveThreadId === undefined) {
|
|
1417
|
+
return errorResult("Error: No active session. Call start_session first.");
|
|
1418
|
+
}
|
|
1419
|
+
const wakeAt = typeof typedArgs.wakeAt === "string" ? new Date(typedArgs.wakeAt).getTime() : undefined;
|
|
1420
|
+
if (wakeAt !== undefined && isNaN(wakeAt)) {
|
|
1421
|
+
return errorResult("Error: Invalid wakeAt timestamp. Use ISO 8601 format.");
|
|
1422
|
+
}
|
|
1423
|
+
// Max sleep: 8 hours
|
|
1424
|
+
const MAX_SLEEP_MS = 8 * 60 * 60 * 1000;
|
|
1425
|
+
const SLEEP_POLL_INTERVAL_MS = 30_000; // 30s
|
|
1426
|
+
const SSE_KEEPALIVE_INTERVAL_MS = 30_000;
|
|
1427
|
+
const deadline = Date.now() + MAX_SLEEP_MS;
|
|
1428
|
+
let lastKeepalive = Date.now();
|
|
1429
|
+
process.stderr.write(`[sleep] Entering sleep mode. threadId=${effectiveThreadId}, wakeAt=${wakeAt ? new Date(wakeAt).toISOString() : "indefinite"}\n`);
|
|
1430
|
+
while (Date.now() < deadline) {
|
|
1431
|
+
// Check for operator messages (non-destructive peek)
|
|
1432
|
+
const peeked = peekThreadMessages(effectiveThreadId);
|
|
1433
|
+
if (peeked.length > 0) {
|
|
1434
|
+
process.stderr.write(`[sleep] Waking up — ${peeked.length} operator message(s) received.\n`);
|
|
1435
|
+
// Don't consume messages — let the next wait_for_instructions call handle them
|
|
1436
|
+
return {
|
|
1437
|
+
content: [{
|
|
1438
|
+
type: "text",
|
|
1439
|
+
text: `Woke up: operator sent a message. Call wait_for_instructions now to read it.` +
|
|
1440
|
+
getShortReminder(effectiveThreadId),
|
|
1441
|
+
}],
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
// Check for scheduled tasks
|
|
1445
|
+
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
1446
|
+
if (dueTask) {
|
|
1447
|
+
process.stderr.write(`[sleep] Waking up — scheduled task fired: ${dueTask.task.label}\n`);
|
|
1448
|
+
// DMN sentinel: generate dynamic first-person reflection
|
|
1449
|
+
const taskPrompt = dueTask.prompt === "__DMN__"
|
|
1450
|
+
? generateDmnReflection(effectiveThreadId)
|
|
1451
|
+
: `⏰ Woke up: scheduled task **"${dueTask.task.label}"**\n\n${dueTask.prompt}`;
|
|
1452
|
+
return {
|
|
1453
|
+
content: [{
|
|
1454
|
+
type: "text",
|
|
1455
|
+
text: taskPrompt + getShortReminder(effectiveThreadId),
|
|
1456
|
+
}],
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
// Check alarm
|
|
1460
|
+
if (wakeAt && Date.now() >= wakeAt) {
|
|
1461
|
+
process.stderr.write(`[sleep] Waking up — alarm reached.\n`);
|
|
1462
|
+
return {
|
|
1463
|
+
content: [{
|
|
1464
|
+
type: "text",
|
|
1465
|
+
text: `Woke up: alarm time reached (${new Date(wakeAt).toISOString()}).` +
|
|
1466
|
+
getShortReminder(effectiveThreadId),
|
|
1467
|
+
}],
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
// SSE keepalive
|
|
1471
|
+
const sinceKeepalive = Date.now() - lastKeepalive;
|
|
1472
|
+
if (sinceKeepalive >= SSE_KEEPALIVE_INTERVAL_MS && extra?.sendNotification) {
|
|
1473
|
+
try {
|
|
1474
|
+
await extra.sendNotification({
|
|
1475
|
+
method: "notifications/message",
|
|
1476
|
+
params: { level: "debug", data: "sleeping", logger: "sensorium" },
|
|
1477
|
+
});
|
|
1478
|
+
lastKeepalive = Date.now();
|
|
1479
|
+
}
|
|
1480
|
+
catch {
|
|
1481
|
+
process.stderr.write(`[sleep] SSE keepalive failed — connection lost.\n`);
|
|
1482
|
+
return {
|
|
1483
|
+
content: [{
|
|
1484
|
+
type: "text",
|
|
1485
|
+
text: "Sleep interrupted: connection lost. Call sleep again to resume." +
|
|
1486
|
+
getShortReminder(effectiveThreadId),
|
|
1487
|
+
}],
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
// Check abort signal
|
|
1492
|
+
if (extra.signal.aborted) {
|
|
1493
|
+
process.stderr.write(`[sleep] SSE connection aborted during sleep.\n`);
|
|
1494
|
+
return {
|
|
1495
|
+
content: [{
|
|
1496
|
+
type: "text",
|
|
1497
|
+
text: "Sleep interrupted: connection closed." +
|
|
1498
|
+
getShortReminder(effectiveThreadId),
|
|
1499
|
+
}],
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_POLL_INTERVAL_MS));
|
|
1503
|
+
}
|
|
1504
|
+
// Max sleep duration reached
|
|
1505
|
+
process.stderr.write(`[sleep] Max sleep duration reached (8h).\n`);
|
|
1506
|
+
return {
|
|
1507
|
+
content: [{
|
|
1508
|
+
type: "text",
|
|
1509
|
+
text: "Woke up: maximum sleep duration reached (8 hours)." +
|
|
1510
|
+
getShortReminder(effectiveThreadId),
|
|
1511
|
+
}],
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1360
1514
|
// ── memory_bootstrap ────────────────────────────────────────────────────
|
|
1361
1515
|
if (name === "memory_bootstrap") {
|
|
1362
1516
|
const threadId = resolveThreadId(args);
|
|
@@ -1472,6 +1626,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1472
1626
|
keywords: Array.isArray(typedArgs.keywords) ? typedArgs.keywords.map(String) : typeof typedArgs.keywords === 'string' ? [typedArgs.keywords] : [],
|
|
1473
1627
|
confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
|
|
1474
1628
|
priority: typeof typedArgs.priority === "number" ? typedArgs.priority : 0,
|
|
1629
|
+
threadId: threadId ?? null,
|
|
1475
1630
|
});
|
|
1476
1631
|
// Fire-and-forget embedding generation
|
|
1477
1632
|
const apiKey = process.env.OPENAI_API_KEY;
|