kernelbot 1.0.37 → 1.0.39

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.
Files changed (41) hide show
  1. package/bin/kernel.js +499 -249
  2. package/config.example.yaml +17 -0
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +3 -1
  6. package/src/agent.js +355 -82
  7. package/src/bot.js +724 -12
  8. package/src/character.js +406 -0
  9. package/src/characters/builder.js +174 -0
  10. package/src/characters/builtins.js +421 -0
  11. package/src/conversation.js +17 -2
  12. package/src/dashboard/agents.css +469 -0
  13. package/src/dashboard/agents.html +184 -0
  14. package/src/dashboard/agents.js +873 -0
  15. package/src/dashboard/dashboard.css +281 -0
  16. package/src/dashboard/dashboard.js +579 -0
  17. package/src/dashboard/index.html +366 -0
  18. package/src/dashboard/server.js +521 -0
  19. package/src/dashboard/shared.css +700 -0
  20. package/src/dashboard/shared.js +218 -0
  21. package/src/life/engine.js +115 -26
  22. package/src/life/evolution.js +7 -5
  23. package/src/life/journal.js +5 -4
  24. package/src/life/memory.js +12 -9
  25. package/src/life/share-queue.js +7 -5
  26. package/src/prompts/orchestrator.js +76 -14
  27. package/src/prompts/workers.js +22 -0
  28. package/src/self.js +17 -5
  29. package/src/services/linkedin-api.js +190 -0
  30. package/src/services/stt.js +8 -2
  31. package/src/services/tts.js +32 -2
  32. package/src/services/x-api.js +141 -0
  33. package/src/swarm/worker-registry.js +7 -0
  34. package/src/tools/categories.js +4 -0
  35. package/src/tools/index.js +6 -0
  36. package/src/tools/linkedin.js +264 -0
  37. package/src/tools/orchestrator-tools.js +337 -2
  38. package/src/tools/x.js +256 -0
  39. package/src/utils/config.js +190 -139
  40. package/src/utils/display.js +165 -52
  41. package/src/utils/temporal-awareness.js +24 -10
@@ -8,6 +8,8 @@ import { definitions as githubDefinitions, handlers as githubHandlers } from './
8
8
  import { definitions as codingDefinitions, handlers as codingHandlers } from './coding.js';
9
9
  import { definitions as browserDefinitions, handlers as browserHandlers } from './browser.js';
10
10
  import { definitions as jiraDefinitions, handlers as jiraHandlers } from './jira.js';
11
+ import { definitions as linkedinDefinitions, handlers as linkedinHandlers } from './linkedin.js';
12
+ import { definitions as xDefinitions, handlers as xHandlers } from './x.js';
11
13
  import { definitions as personaDefinitions, handlers as personaHandlers } from './persona.js';
12
14
  import { logToolCall } from '../security/audit.js';
13
15
  import { requiresConfirmation } from '../security/confirm.js';
@@ -23,6 +25,8 @@ export const toolDefinitions = [
23
25
  ...codingDefinitions,
24
26
  ...browserDefinitions,
25
27
  ...jiraDefinitions,
28
+ ...linkedinDefinitions,
29
+ ...xDefinitions,
26
30
  ...personaDefinitions,
27
31
  ];
28
32
 
@@ -37,6 +41,8 @@ const handlerMap = {
37
41
  ...codingHandlers,
38
42
  ...browserHandlers,
39
43
  ...jiraHandlers,
44
+ ...linkedinHandlers,
45
+ ...xHandlers,
40
46
  ...personaHandlers,
41
47
  };
42
48
 
@@ -0,0 +1,264 @@
1
+ import { LinkedInAPI } from '../services/linkedin-api.js';
2
+ import { getLogger } from '../utils/logger.js';
3
+
4
+ /**
5
+ * Get a configured LinkedIn API client from the tool context.
6
+ * Expects context.config.linkedin.access_token and context.config.linkedin.person_urn
7
+ * to be injected at dispatch time.
8
+ */
9
+ function getClient(context) {
10
+ const token = context.config.linkedin?.access_token;
11
+ if (!token) throw new Error('LinkedIn not connected. Use /linkedin link to connect your account.');
12
+ return new LinkedInAPI(token);
13
+ }
14
+
15
+ function getPersonUrn(context) {
16
+ const urn = context.config.linkedin?.person_urn;
17
+ if (!urn) throw new Error('LinkedIn person URN not available. Try /linkedin link again.');
18
+ return urn;
19
+ }
20
+
21
+ export const definitions = [
22
+ {
23
+ name: 'linkedin_create_post',
24
+ description: 'Create a LinkedIn post. Can be text-only or include an article link.',
25
+ input_schema: {
26
+ type: 'object',
27
+ properties: {
28
+ text: {
29
+ type: 'string',
30
+ description: 'The post content/commentary text',
31
+ },
32
+ visibility: {
33
+ type: 'string',
34
+ enum: ['PUBLIC', 'CONNECTIONS'],
35
+ description: 'Post visibility (default: PUBLIC)',
36
+ },
37
+ article_url: {
38
+ type: 'string',
39
+ description: 'Optional URL to share as an article attachment',
40
+ },
41
+ article_title: {
42
+ type: 'string',
43
+ description: 'Optional title for the shared article',
44
+ },
45
+ },
46
+ required: ['text'],
47
+ },
48
+ },
49
+ {
50
+ name: 'linkedin_get_my_posts',
51
+ description: 'Get the user\'s recent LinkedIn posts. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
52
+ input_schema: {
53
+ type: 'object',
54
+ properties: {
55
+ count: {
56
+ type: 'number',
57
+ description: 'Number of posts to fetch (default 10)',
58
+ },
59
+ },
60
+ },
61
+ },
62
+ {
63
+ name: 'linkedin_get_post',
64
+ description: 'Get a specific LinkedIn post by its URN. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
65
+ input_schema: {
66
+ type: 'object',
67
+ properties: {
68
+ post_urn: {
69
+ type: 'string',
70
+ description: 'The LinkedIn post URN (e.g. urn:li:share:12345)',
71
+ },
72
+ },
73
+ required: ['post_urn'],
74
+ },
75
+ },
76
+ {
77
+ name: 'linkedin_comment_on_post',
78
+ description: 'Add a comment to a LinkedIn post.',
79
+ input_schema: {
80
+ type: 'object',
81
+ properties: {
82
+ post_urn: {
83
+ type: 'string',
84
+ description: 'The LinkedIn post URN to comment on',
85
+ },
86
+ comment: {
87
+ type: 'string',
88
+ description: 'The comment text',
89
+ },
90
+ },
91
+ required: ['post_urn', 'comment'],
92
+ },
93
+ },
94
+ {
95
+ name: 'linkedin_get_comments',
96
+ description: 'Get comments on a LinkedIn post. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
97
+ input_schema: {
98
+ type: 'object',
99
+ properties: {
100
+ post_urn: {
101
+ type: 'string',
102
+ description: 'The LinkedIn post URN',
103
+ },
104
+ count: {
105
+ type: 'number',
106
+ description: 'Number of comments to fetch (default 10)',
107
+ },
108
+ },
109
+ required: ['post_urn'],
110
+ },
111
+ },
112
+ {
113
+ name: 'linkedin_like_post',
114
+ description: 'Like a LinkedIn post.',
115
+ input_schema: {
116
+ type: 'object',
117
+ properties: {
118
+ post_urn: {
119
+ type: 'string',
120
+ description: 'The LinkedIn post URN to like',
121
+ },
122
+ },
123
+ required: ['post_urn'],
124
+ },
125
+ },
126
+ {
127
+ name: 'linkedin_get_profile',
128
+ description: 'Get the linked LinkedIn profile information. NOTE: Requires "Sign in with LinkedIn" product (openid + profile scopes). Returns limited info if only w_member_social is available.',
129
+ input_schema: {
130
+ type: 'object',
131
+ properties: {},
132
+ },
133
+ },
134
+ {
135
+ name: 'linkedin_delete_post',
136
+ description: 'Delete a LinkedIn post.',
137
+ input_schema: {
138
+ type: 'object',
139
+ properties: {
140
+ post_urn: {
141
+ type: 'string',
142
+ description: 'The LinkedIn post URN to delete',
143
+ },
144
+ },
145
+ required: ['post_urn'],
146
+ },
147
+ },
148
+ ];
149
+
150
+ export const handlers = {
151
+ linkedin_create_post: async (params, context) => {
152
+ try {
153
+ const client = getClient(context);
154
+ const authorUrn = getPersonUrn(context);
155
+ const visibility = params.visibility || 'PUBLIC';
156
+
157
+ let result;
158
+ if (params.article_url) {
159
+ result = await client.createArticlePost(authorUrn, params.text, params.article_url, params.article_title);
160
+ } else {
161
+ result = await client.createTextPost(authorUrn, params.text, visibility);
162
+ }
163
+
164
+ return { success: true, message: 'Post created successfully', post: result };
165
+ } catch (err) {
166
+ getLogger().error(`linkedin_create_post failed: ${err.message}`);
167
+ return { error: err.response?.data?.message || err.message };
168
+ }
169
+ },
170
+
171
+ linkedin_get_my_posts: async (params, context) => {
172
+ try {
173
+ const client = getClient(context);
174
+ const authorUrn = getPersonUrn(context);
175
+ const posts = await client.getMyPosts(authorUrn, params.count || 10);
176
+ return { posts, count: posts.length };
177
+ } catch (err) {
178
+ getLogger().error(`linkedin_get_my_posts failed: ${err.message}`);
179
+ if (err.response?.status === 403) {
180
+ return { error: 'Access denied — reading posts requires r_member_social permission which is restricted to approved LinkedIn apps. Your token only has w_member_social (write-only: post, comment, like).' };
181
+ }
182
+ return { error: err.response?.data?.message || err.message };
183
+ }
184
+ },
185
+
186
+ linkedin_get_post: async (params, context) => {
187
+ try {
188
+ const client = getClient(context);
189
+ const post = await client.getPost(params.post_urn);
190
+ return { post };
191
+ } catch (err) {
192
+ getLogger().error(`linkedin_get_post failed: ${err.message}`);
193
+ if (err.response?.status === 403) {
194
+ return { error: 'Access denied — reading posts requires r_member_social permission which is restricted to approved LinkedIn apps.' };
195
+ }
196
+ return { error: err.response?.data?.message || err.message };
197
+ }
198
+ },
199
+
200
+ linkedin_comment_on_post: async (params, context) => {
201
+ try {
202
+ const client = getClient(context);
203
+ const actorUrn = getPersonUrn(context);
204
+ const result = await client.addComment(params.post_urn, params.comment, actorUrn);
205
+ return { success: true, message: 'Comment posted', comment: result };
206
+ } catch (err) {
207
+ getLogger().error(`linkedin_comment_on_post failed: ${err.message}`);
208
+ return { error: err.response?.data?.message || err.message };
209
+ }
210
+ },
211
+
212
+ linkedin_get_comments: async (params, context) => {
213
+ try {
214
+ const client = getClient(context);
215
+ const comments = await client.getComments(params.post_urn, params.count || 10);
216
+ return { comments, count: comments.length };
217
+ } catch (err) {
218
+ getLogger().error(`linkedin_get_comments failed: ${err.message}`);
219
+ if (err.response?.status === 403) {
220
+ return { error: 'Access denied — reading comments requires r_member_social permission which is restricted to approved LinkedIn apps.' };
221
+ }
222
+ return { error: err.response?.data?.message || err.message };
223
+ }
224
+ },
225
+
226
+ linkedin_like_post: async (params, context) => {
227
+ try {
228
+ const client = getClient(context);
229
+ const actorUrn = getPersonUrn(context);
230
+ const result = await client.likePost(params.post_urn, actorUrn);
231
+ return { success: true, message: 'Post liked', result };
232
+ } catch (err) {
233
+ getLogger().error(`linkedin_like_post failed: ${err.message}`);
234
+ return { error: err.response?.data?.message || err.message };
235
+ }
236
+ },
237
+
238
+ linkedin_get_profile: async (params, context) => {
239
+ try {
240
+ const client = getClient(context);
241
+ const profile = await client.getProfile();
242
+ if (!profile) {
243
+ // No openid+profile scopes — return what we know from config
244
+ const urn = context.config.linkedin?.person_urn;
245
+ return { profile: { person_urn: urn }, note: 'Full profile unavailable — LinkedIn app only has w_member_social scope. Add "Sign in with LinkedIn" product for full profile access.' };
246
+ }
247
+ return { profile };
248
+ } catch (err) {
249
+ getLogger().error(`linkedin_get_profile failed: ${err.message}`);
250
+ return { error: err.response?.data?.message || err.message };
251
+ }
252
+ },
253
+
254
+ linkedin_delete_post: async (params, context) => {
255
+ try {
256
+ const client = getClient(context);
257
+ await client.deletePost(params.post_urn);
258
+ return { success: true, message: 'Post deleted' };
259
+ } catch (err) {
260
+ getLogger().error(`linkedin_delete_post failed: ${err.message}`);
261
+ return { error: err.response?.data?.message || err.message };
262
+ }
263
+ },
264
+ };
@@ -5,7 +5,35 @@ import { getLogger } from '../utils/logger.js';
5
5
  const workerTypeEnum = Object.keys(WORKER_TYPES);
6
6
 
7
7
  /**
8
- * Tool definitions for the orchestrator's 3 meta-tools.
8
+ * Tokenize a task string for similarity comparison.
9
+ * Lowercase, strip punctuation, filter words ≤2 chars.
10
+ */
11
+ function tokenize(text) {
12
+ return text
13
+ .toLowerCase()
14
+ .replace(/[^\w\s]/g, ' ')
15
+ .split(/\s+/)
16
+ .filter(w => w.length > 2);
17
+ }
18
+
19
+ /**
20
+ * Jaccard word-overlap similarity between two task strings.
21
+ * Returns a value between 0 and 1.
22
+ */
23
+ function taskSimilarity(a, b) {
24
+ const setA = new Set(tokenize(a));
25
+ const setB = new Set(tokenize(b));
26
+ if (setA.size === 0 && setB.size === 0) return 1;
27
+ if (setA.size === 0 || setB.size === 0) return 0;
28
+ let intersection = 0;
29
+ for (const word of setA) {
30
+ if (setB.has(word)) intersection++;
31
+ }
32
+ return intersection / (setA.size + setB.size - intersection);
33
+ }
34
+
35
+ /**
36
+ * Tool definitions for the orchestrator's meta-tools.
9
37
  */
10
38
  export const orchestratorToolDefinitions = [
11
39
  {
@@ -58,6 +86,20 @@ export const orchestratorToolDefinitions = [
58
86
  required: ['job_id'],
59
87
  },
60
88
  },
89
+ {
90
+ name: 'check_job',
91
+ description: 'Get detailed diagnostics for a specific job — status, elapsed time, activity log, and stuck detection. Use this to investigate workers that seem slow or unresponsive.',
92
+ input_schema: {
93
+ type: 'object',
94
+ properties: {
95
+ job_id: {
96
+ type: 'string',
97
+ description: 'The ID of the job to check.',
98
+ },
99
+ },
100
+ required: ['job_id'],
101
+ },
102
+ },
61
103
  {
62
104
  name: 'create_automation',
63
105
  description: 'Create a recurring automation that runs on a schedule. The task description will be executed as a standalone prompt each time it fires.',
@@ -176,6 +218,60 @@ export const orchestratorToolDefinitions = [
176
218
  required: ['emoji'],
177
219
  },
178
220
  },
221
+ {
222
+ name: 'recall_memories',
223
+ description: 'Search your episodic and semantic memory for information about a topic, event, or past experience. Use this when the user references something from the past or you need context about a specific subject.',
224
+ input_schema: {
225
+ type: 'object',
226
+ properties: {
227
+ query: {
228
+ type: 'string',
229
+ description: 'Keywords or topic to search memories for (e.g. "kubernetes", "birthday", "project deadline").',
230
+ },
231
+ time_range_hours: {
232
+ type: 'number',
233
+ description: 'Optional: limit episodic search to the last N hours. Defaults to searching all available memories.',
234
+ },
235
+ },
236
+ required: ['query'],
237
+ },
238
+ },
239
+ {
240
+ name: 'recall_user_history',
241
+ description: 'Retrieve memories specifically about a user — past interactions, preferences, things they told you. Use when you need to remember what a specific person has shared or discussed.',
242
+ input_schema: {
243
+ type: 'object',
244
+ properties: {
245
+ user_id: {
246
+ type: 'string',
247
+ description: 'The user ID to retrieve memories for.',
248
+ },
249
+ limit: {
250
+ type: 'number',
251
+ description: 'Maximum number of memories to return. Default: 10.',
252
+ },
253
+ },
254
+ required: ['user_id'],
255
+ },
256
+ },
257
+ {
258
+ name: 'search_conversations',
259
+ description: 'Search past messages in the current (or specified) chat for a keyword or phrase. Use when you need to find something specific that was said in conversation.',
260
+ input_schema: {
261
+ type: 'object',
262
+ properties: {
263
+ query: {
264
+ type: 'string',
265
+ description: 'Keyword or phrase to search for in conversation history.',
266
+ },
267
+ chat_id: {
268
+ type: 'string',
269
+ description: 'Optional: chat ID to search. Defaults to the current chat.',
270
+ },
271
+ },
272
+ required: ['query'],
273
+ },
274
+ },
179
275
  ];
180
276
 
181
277
  /**
@@ -259,6 +355,26 @@ export async function executeOrchestratorTool(name, input, context) {
259
355
  }
260
356
  }
261
357
 
358
+ // Duplicate task detection — compare against running + queued jobs in this chat
359
+ const activeJobs = jobManager.getJobsForChat(chatId)
360
+ .filter(j => (j.status === 'running' || j.status === 'queued') && j.workerType === worker_type);
361
+
362
+ for (const existing of activeJobs) {
363
+ const sim = taskSimilarity(task, existing.task);
364
+ if (sim > 0.7) {
365
+ logger.warn(`[dispatch_task] Duplicate blocked — ${(sim * 100).toFixed(0)}% similar to job ${existing.id}: "${existing.task.slice(0, 80)}"`);
366
+ return {
367
+ error: `A very similar ${worker_type} task is already ${existing.status} (job ${existing.id}, ${(sim * 100).toFixed(0)}% match). Wait for it to finish or cancel it first.`,
368
+ existing_job_id: existing.id,
369
+ similarity: Math.round(sim * 100),
370
+ };
371
+ }
372
+ if (sim > 0.4) {
373
+ logger.info(`[dispatch_task] Similar task warning — ${(sim * 100).toFixed(0)}% overlap with job ${existing.id}`);
374
+ credentialWarning = [credentialWarning, `Warning: This task is ${(sim * 100).toFixed(0)}% similar to an active ${worker_type} job (${existing.id}). Proceeding anyway.`].filter(Boolean).join('\n');
375
+ }
376
+ }
377
+
262
378
  // Create the job with context and dependencies
263
379
  const job = jobManager.createJob(chatId, worker_type, task);
264
380
  job.context = [taskContext, credentialWarning].filter(Boolean).join('\n\n') || null;
@@ -288,12 +404,21 @@ export async function executeOrchestratorTool(name, input, context) {
288
404
  }
289
405
  });
290
406
 
291
- return {
407
+ // Collect sibling active jobs for awareness
408
+ const siblingJobs = jobManager.getJobsForChat(chatId)
409
+ .filter(j => j.id !== job.id && (j.status === 'running' || j.status === 'queued'))
410
+ .map(j => ({ id: j.id, worker_type: j.workerType, status: j.status, task: j.task.slice(0, 80) }));
411
+
412
+ const result = {
292
413
  job_id: job.id,
293
414
  worker_type,
294
415
  status: 'dispatched',
295
416
  message: `${workerConfig.emoji} ${workerConfig.label} started.`,
296
417
  };
418
+ if (siblingJobs.length > 0) {
419
+ result.other_active_jobs = siblingJobs;
420
+ }
421
+ return result;
297
422
  }
298
423
 
299
424
  case 'list_jobs': {
@@ -345,6 +470,68 @@ export async function executeOrchestratorTool(name, input, context) {
345
470
  };
346
471
  }
347
472
 
473
+ case 'check_job': {
474
+ const { job_id } = input;
475
+ logger.info(`[check_job] Checking job ${job_id}`);
476
+ const job = jobManager.getJob(job_id);
477
+ if (!job) {
478
+ return { error: `Job ${job_id} not found.` };
479
+ }
480
+
481
+ const now = Date.now();
482
+ const elapsed = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
483
+ const timeoutSec = job.timeoutMs ? Math.round(job.timeoutMs / 1000) : null;
484
+ const timeRemaining = (timeoutSec && job.startedAt) ? Math.max(0, timeoutSec - elapsed) : null;
485
+
486
+ const result = {
487
+ job_id: job.id,
488
+ worker_type: job.workerType,
489
+ status: job.status,
490
+ task: job.task.slice(0, 200),
491
+ elapsed_seconds: elapsed,
492
+ timeout_seconds: timeoutSec,
493
+ time_remaining_seconds: timeRemaining,
494
+ llm_calls: job.llmCalls,
495
+ tool_calls: job.toolCalls,
496
+ last_thinking: job.lastThinking ? job.lastThinking.slice(0, 300) : null,
497
+ recent_activity: job.progress.slice(-10),
498
+ last_activity_seconds_ago: job.lastActivity ? Math.round((now - job.lastActivity) / 1000) : null,
499
+ };
500
+
501
+ // Stuck detection diagnostics (only for running jobs)
502
+ // Long-running workers (coding, devops) get higher thresholds
503
+ if (job.status === 'running') {
504
+ const diagnostics = [];
505
+ const isLongRunning = ['coding', 'devops'].includes(job.workerType);
506
+ const idleThreshold = isLongRunning ? 600 : 120;
507
+ const loopLlmThreshold = isLongRunning ? 50 : 15;
508
+
509
+ const idleSec = job.lastActivity ? Math.round((now - job.lastActivity) / 1000) : elapsed;
510
+ if (idleSec > idleThreshold) {
511
+ diagnostics.push(`IDLE for ${idleSec}s — no activity detected`);
512
+ }
513
+ if (job.llmCalls > loopLlmThreshold && job.toolCalls < 3) {
514
+ diagnostics.push(`POSSIBLY LOOPING — ${job.llmCalls} LLM calls but only ${job.toolCalls} tool calls`);
515
+ }
516
+ if (timeoutSec && elapsed > timeoutSec * 0.75) {
517
+ const pct = Math.round((elapsed / timeoutSec) * 100);
518
+ diagnostics.push(`${pct}% of timeout used (${elapsed}s / ${timeoutSec}s)`);
519
+ }
520
+ if (diagnostics.length > 0) {
521
+ result.warnings = diagnostics;
522
+ }
523
+ }
524
+
525
+ if (job.dependsOn.length > 0) result.depends_on = job.dependsOn;
526
+ if (job.error) result.error = job.error;
527
+ if (job.structuredResult) {
528
+ result.result_summary = job.structuredResult.summary;
529
+ result.result_status = job.structuredResult.status;
530
+ }
531
+
532
+ return result;
533
+ }
534
+
348
535
  case 'create_automation': {
349
536
  if (!automationManager) return { error: 'Automation system not available.' };
350
537
 
@@ -479,6 +666,154 @@ export async function executeOrchestratorTool(name, input, context) {
479
666
  }
480
667
  }
481
668
 
669
+ case 'recall_memories': {
670
+ const { memoryManager } = context;
671
+ if (!memoryManager) return { error: 'Memory system not available.' };
672
+
673
+ const { query, time_range_hours } = input;
674
+ logger.info(`[recall_memories] Searching for: "${query}" (time_range=${time_range_hours || 'all'})`);
675
+
676
+ const episodic = memoryManager.searchEpisodic(query, 10);
677
+ const semantic = memoryManager.searchSemantic(query, 5);
678
+
679
+ // Filter by time range if specified
680
+ const now = Date.now();
681
+ const filteredEpisodic = time_range_hours
682
+ ? episodic.filter(m => (now - m.timestamp) <= time_range_hours * 3600_000)
683
+ : episodic;
684
+
685
+ const formatAge = (ts) => {
686
+ const ageH = Math.round((now - ts) / 3600_000);
687
+ if (ageH < 1) return 'just now';
688
+ if (ageH < 24) return `${ageH}h ago`;
689
+ const ageD = Math.round(ageH / 24);
690
+ return `${ageD}d ago`;
691
+ };
692
+
693
+ const episodicResults = filteredEpisodic.map(m => ({
694
+ summary: m.summary,
695
+ age: formatAge(m.timestamp),
696
+ importance: m.importance,
697
+ tags: m.tags,
698
+ type: m.type,
699
+ }));
700
+
701
+ const semanticResults = semantic.map(s => ({
702
+ topic: s.topic,
703
+ summary: s.summary,
704
+ learned: formatAge(s.learnedAt),
705
+ related: s.relatedTopics,
706
+ }));
707
+
708
+ logger.info(`[recall_memories] Found ${episodicResults.length} episodic, ${semanticResults.length} semantic results for query="${query}"`);
709
+
710
+ // Detailed logging — show what was actually recalled
711
+ for (const m of episodicResults) {
712
+ logger.info(`[recall_memories] 📝 [${m.age}, imp=${m.importance}] ${m.summary.slice(0, 120)}`);
713
+ }
714
+ for (const s of semanticResults) {
715
+ logger.info(`[recall_memories] 🧠 [${s.topic}] ${s.summary.slice(0, 120)}`);
716
+ }
717
+
718
+ if (episodicResults.length === 0 && semanticResults.length === 0) {
719
+ logger.info(`[recall_memories] No results found for query="${query}"`);
720
+ return { message: `No memories found matching "${query}".` };
721
+ }
722
+
723
+ return {
724
+ query,
725
+ episodic: episodicResults,
726
+ semantic: semanticResults,
727
+ };
728
+ }
729
+
730
+ case 'recall_user_history': {
731
+ const { memoryManager } = context;
732
+ if (!memoryManager) return { error: 'Memory system not available.' };
733
+
734
+ const { user_id, limit } = input;
735
+ logger.info(`[recall_user_history] Retrieving memories for user: ${user_id} (limit=${limit || 10})`);
736
+
737
+ const memories = memoryManager.getMemoriesAboutUser(user_id, limit || 10);
738
+
739
+ const now = Date.now();
740
+ const results = memories.map(m => ({
741
+ summary: m.summary,
742
+ age: (() => {
743
+ const ageH = Math.round((now - m.timestamp) / 3600_000);
744
+ if (ageH < 1) return 'just now';
745
+ if (ageH < 24) return `${ageH}h ago`;
746
+ return `${Math.round(ageH / 24)}d ago`;
747
+ })(),
748
+ importance: m.importance,
749
+ type: m.type,
750
+ tags: m.tags,
751
+ }));
752
+
753
+ logger.info(`[recall_user_history] Found ${results.length} memories for user ${user_id}`);
754
+
755
+ // Detailed logging — show each recalled memory
756
+ for (const m of results) {
757
+ logger.info(`[recall_user_history] 📝 [${m.age}, imp=${m.importance}, ${m.type}] ${m.summary.slice(0, 120)}`);
758
+ }
759
+
760
+ if (results.length === 0) {
761
+ logger.info(`[recall_user_history] No memories found for user ${user_id}`);
762
+ return { message: `No memories found for user ${user_id}.` };
763
+ }
764
+
765
+ return { user_id, memories: results };
766
+ }
767
+
768
+ case 'search_conversations': {
769
+ const { conversationManager } = context;
770
+ if (!conversationManager) return { error: 'Conversation system not available.' };
771
+
772
+ const { query, chat_id } = input;
773
+ const targetChatId = chat_id || chatId;
774
+ logger.info(`[search_conversations] Searching chat ${targetChatId} for: "${query}"`);
775
+
776
+ const history = conversationManager.getHistory(targetChatId);
777
+ const queryLower = query.toLowerCase();
778
+
779
+ const matches = [];
780
+ for (let i = 0; i < history.length; i++) {
781
+ const msg = history[i];
782
+ const content = typeof msg.content === 'string' ? msg.content : '';
783
+ if (content.toLowerCase().includes(queryLower)) {
784
+ matches.push({
785
+ role: msg.role,
786
+ snippet: content.length > 300 ? content.slice(0, 300) + '...' : content,
787
+ age: msg.timestamp
788
+ ? (() => {
789
+ const ageH = Math.round((Date.now() - msg.timestamp) / 3600_000);
790
+ if (ageH < 1) return 'just now';
791
+ if (ageH < 24) return `${ageH}h ago`;
792
+ return `${Math.round(ageH / 24)}d ago`;
793
+ })()
794
+ : 'unknown',
795
+ });
796
+ }
797
+ }
798
+
799
+ // Return last 10 matches (most recent)
800
+ const results = matches.slice(-10);
801
+
802
+ logger.info(`[search_conversations] Found ${matches.length} total matches in chat ${targetChatId}, returning last ${results.length}`);
803
+
804
+ // Detailed logging — show matched conversation snippets
805
+ for (const m of results) {
806
+ logger.info(`[search_conversations] 💬 [${m.role}, ${m.age}] ${m.snippet.slice(0, 100)}`);
807
+ }
808
+
809
+ if (results.length === 0) {
810
+ logger.info(`[search_conversations] No matches for "${query}" in chat ${targetChatId}`);
811
+ return { message: `No messages found matching "${query}" in this chat.` };
812
+ }
813
+
814
+ return { query, chat_id: targetChatId, matches: results, total_matches: matches.length };
815
+ }
816
+
482
817
  default:
483
818
  return { error: `Unknown orchestrator tool: ${name}` };
484
819
  }