morpheus-cli 0.9.12 → 0.9.13

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/channels/telegram.js +3 -3
  3. package/dist/cli/commands/restart.js +2 -2
  4. package/dist/cli/commands/start.js +2 -2
  5. package/dist/http/api.js +5 -2
  6. package/dist/http/routers/agents.js +9 -0
  7. package/dist/http/routers/link.js +2 -2
  8. package/dist/runtime/chronos/repository.js +5 -3
  9. package/dist/runtime/chronos/worker.js +27 -4
  10. package/dist/runtime/chronos/worker.test.js +3 -2
  11. package/dist/runtime/hot-reload.js +3 -3
  12. package/dist/runtime/memory/sqlite.js +37 -11
  13. package/dist/runtime/oracle.js +78 -43
  14. package/dist/runtime/{apoc.js → subagents/apoc.js} +19 -6
  15. package/dist/runtime/{devkit-instrument.js → subagents/devkit-instrument.js} +1 -1
  16. package/dist/runtime/subagents/index.js +12 -0
  17. package/dist/runtime/{link.js → subagents/link/link.js} +23 -9
  18. package/dist/runtime/{link-repository.js → subagents/link/repository.js} +2 -2
  19. package/dist/runtime/{link-search.js → subagents/link/search.js} +3 -3
  20. package/dist/runtime/{link-worker.js → subagents/link/worker.js} +6 -6
  21. package/dist/runtime/{neo.js → subagents/neo.js} +23 -9
  22. package/dist/runtime/subagents/registry.js +134 -0
  23. package/dist/runtime/{trinity.js → subagents/trinity/trinity.js} +22 -8
  24. package/dist/runtime/{subagent-utils.js → subagents/utils.js} +2 -2
  25. package/dist/runtime/tasks/worker.js +6 -70
  26. package/dist/runtime/tools/chronos-tools.js +1 -0
  27. package/dist/runtime/tools/morpheus-tools.js +3 -3
  28. package/dist/runtime/webhooks/dispatcher.js +4 -0
  29. package/dist/ui/assets/AuditDashboard-BVyKnpVm.js +1 -0
  30. package/dist/ui/assets/Chat-UVoDlqqM.js +41 -0
  31. package/dist/ui/assets/{Chronos-D1yAb4M5.js → Chronos-Dfs_pOsc.js} +1 -1
  32. package/dist/ui/assets/{ConfirmationModal-DxUHZgTy.js → ConfirmationModal-BBIjVef7.js} +1 -1
  33. package/dist/ui/assets/{Dashboard-BzxmcHaS.js → Dashboard-BdSQDB14.js} +1 -1
  34. package/dist/ui/assets/{DeleteConfirmationModal-CqNXT_YQ.js → DeleteConfirmationModal-Du85q5u2.js} +1 -1
  35. package/dist/ui/assets/{Documents-DLFZdmim.js → Documents-DguILrI8.js} +1 -1
  36. package/dist/ui/assets/{Logs-B1Bpy9dB.js → Logs-BDup2FET.js} +1 -1
  37. package/dist/ui/assets/{MCPManager-BbUDMh5Q.js → MCPManager-WBdh1rum.js} +1 -1
  38. package/dist/ui/assets/{ModelPricing-DCl-2_eJ.js → ModelPricing-BQPw0r6z.js} +1 -1
  39. package/dist/ui/assets/{Notifications-8Cqj-mNp.js → Notifications-BslO2Ect.js} +1 -1
  40. package/dist/ui/assets/{SatiMemories-CdHUe6di.js → SatiMemories-DzaLaZ6M.js} +1 -1
  41. package/dist/ui/assets/SessionAudit-CBDThjBi.js +9 -0
  42. package/dist/ui/assets/{Settings-Clge45Z0.js → Settings-JPTCA7C7.js} +1 -1
  43. package/dist/ui/assets/{Skills-0k7A2T5_.js → Skills-BnDg1HCb.js} +1 -1
  44. package/dist/ui/assets/{Smiths-gjgBMN1F.js → Smiths-DR6g_o3D.js} +1 -1
  45. package/dist/ui/assets/Tasks-BuoNCvI-.js +1 -0
  46. package/dist/ui/assets/{TrinityDatabases-CGna6IMX.js → TrinityDatabases-DYHJunk7.js} +1 -1
  47. package/dist/ui/assets/{UsageStats-B7EzZlZe.js → UsageStats-BpGXaHgW.js} +1 -1
  48. package/dist/ui/assets/{WebhookManager-Bb7KiucS.js → WebhookManager-D2muhYy9.js} +1 -1
  49. package/dist/ui/assets/agents-CgqJea9n.js +1 -0
  50. package/dist/ui/assets/{audit-CJ2Ms81U.js → audit-Dc3YW0-4.js} +1 -1
  51. package/dist/ui/assets/{chronos-Bm68OSy4.js → chronos-CZvGhZQB.js} +1 -1
  52. package/dist/ui/assets/{config-C88yQ_CP.js → config-pKL8Y4V9.js} +1 -1
  53. package/dist/ui/assets/{index-BxN2w9sY.js → index-Bta9YXEm.js} +2 -2
  54. package/dist/ui/assets/index-Cjli-AD7.css +1 -0
  55. package/dist/ui/assets/{mcp-BE_OVkBe.js → mcp-vIffcwd6.js} +1 -1
  56. package/dist/ui/assets/{skills-Dt0qU4gH.js → skills-wANsorUj.js} +1 -1
  57. package/dist/ui/assets/{stats-Bmdps1LR.js → stats-xnlA4NwX.js} +1 -1
  58. package/dist/ui/index.html +2 -2
  59. package/dist/ui/sw.js +1 -1
  60. package/package.json +1 -1
  61. package/dist/ui/assets/AuditDashboard-CM1YN1uk.js +0 -1
  62. package/dist/ui/assets/Chat-D4y-g6Tw.js +0 -41
  63. package/dist/ui/assets/SessionAudit-CtVHK_IH.js +0 -9
  64. package/dist/ui/assets/Tasks-AQ3MrrMp.js +0 -1
  65. package/dist/ui/assets/index-C3Ff736M.css +0 -1
  66. /package/dist/runtime/{ISubagent.js → subagents/ISubagent.js} +0 -0
  67. /package/dist/runtime/{link-chunker.js → subagents/link/chunker.js} +0 -0
  68. /package/dist/runtime/{trinity-connector.js → subagents/trinity/connector.js} +0 -0
package/README.md CHANGED
@@ -9,7 +9,7 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
9
9
 
10
10
  ## Why Morpheus
11
11
  - Local-first persistence (sessions, messages, usage, tasks).
12
- - Multi-agent architecture (Oracle, Neo, Apoc, Sati, Trinity, Smith).
12
+ - Multi-agent architecture (Oracle, Neo, Apoc, Sati, Trinity, Link, Smith) with centralized SubagentRegistry.
13
13
  - Async task execution with queue + worker + notifier.
14
14
  - Chronos temporal scheduler for recurring and one-time Oracle executions.
15
15
  - Smith remote agent system for DevKit execution on isolated machines via WebSocket.
@@ -634,7 +634,7 @@ export class TelegramAdapter {
634
634
  await ctx.answerCbQuery('Testing connection…').catch(() => { });
635
635
  try {
636
636
  const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
637
- const { testConnection } = await import('../runtime/trinity-connector.js');
637
+ const { testConnection } = await import('../runtime/subagents/trinity/connector.js');
638
638
  const db = DatabaseRegistry.getInstance().getDatabase(id);
639
639
  if (!db) {
640
640
  await safeReply(ctx, '❌ Database not found.');
@@ -666,8 +666,8 @@ export class TelegramAdapter {
666
666
  await ctx.answerCbQuery('Refreshing schema…').catch(() => { });
667
667
  try {
668
668
  const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
669
- const { introspectSchema } = await import('../runtime/trinity-connector.js');
670
- const { Trinity } = await import('../runtime/trinity.js');
669
+ const { introspectSchema } = await import('../runtime/subagents/trinity/connector.js');
670
+ const { Trinity } = await import('../runtime/subagents/trinity/trinity.js');
671
671
  const registry = DatabaseRegistry.getInstance();
672
672
  const db = registry.getDatabase(id);
673
673
  if (!db) {
@@ -15,8 +15,8 @@ import { HttpServer } from '../../http/server.js';
15
15
  import { getVersion } from '../utils/version.js';
16
16
  import { TaskWorker } from '../../runtime/tasks/worker.js';
17
17
  import { TaskNotifier } from '../../runtime/tasks/notifier.js';
18
- import { Link } from '../../runtime/link.js';
19
- import { LinkWorker } from '../../runtime/link-worker.js';
18
+ import { Link } from '../../runtime/subagents/link/link.js';
19
+ import { LinkWorker } from '../../runtime/subagents/link/worker.js';
20
20
  export const restartCommand = new Command('restart')
21
21
  .description('Restart the Morpheus agent')
22
22
  .option('--ui', 'Enable web UI', true)
@@ -26,8 +26,8 @@ import { ChronosRepository } from '../../runtime/chronos/repository.js';
26
26
  import { SkillRegistry } from '../../runtime/skills/index.js';
27
27
  import { MCPToolCache } from '../../runtime/tools/cache.js';
28
28
  import { SmithRegistry } from '../../runtime/smiths/registry.js';
29
- import { Link } from '../../runtime/link.js';
30
- import { LinkWorker } from '../../runtime/link-worker.js';
29
+ import { Link } from '../../runtime/subagents/link/link.js';
30
+ import { LinkWorker } from '../../runtime/subagents/link/worker.js';
31
31
  // Load .env file explicitly in start command
32
32
  const envPath = path.join(process.cwd(), '.env');
33
33
  if (fs.existsSync(envPath)) {
package/dist/http/api.js CHANGED
@@ -13,8 +13,8 @@ import { MCPServerConfigSchema } from '../config/schemas.js';
13
13
  import { Construtor } from '../runtime/tools/factory.js';
14
14
  import { TaskRepository } from '../runtime/tasks/repository.js';
15
15
  import { DatabaseRegistry } from '../runtime/memory/trinity-db.js';
16
- import { testConnection, introspectSchema } from '../runtime/trinity-connector.js';
17
- import { Trinity } from '../runtime/trinity.js';
16
+ import { testConnection, introspectSchema } from '../runtime/subagents/trinity/connector.js';
17
+ import { Trinity } from '../runtime/subagents/trinity/trinity.js';
18
18
  import { ChronosRepository } from '../runtime/chronos/repository.js';
19
19
  import { ChronosWorker } from '../runtime/chronos/worker.js';
20
20
  import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
@@ -22,6 +22,7 @@ import { createSkillsRouter } from './routers/skills.js';
22
22
  import { createSmithsRouter } from './routers/smiths.js';
23
23
  import { createDangerRouter } from './routers/danger.js';
24
24
  import { createLinkRouter } from './routers/link.js';
25
+ import { createAgentsRouter } from './routers/agents.js';
25
26
  import { getActiveEnvOverrides } from '../config/precedence.js';
26
27
  import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
27
28
  import { AuditRepository } from '../runtime/audit/repository.js';
@@ -55,6 +56,8 @@ export function createApiRouter(oracle, chronosWorker) {
55
56
  router.use('/danger', createDangerRouter());
56
57
  // Mount Link router (Documentation management)
57
58
  router.use('/link', createLinkRouter());
59
+ // Mount Agents metadata router
60
+ router.use('/agents', createAgentsRouter());
58
61
  // --- Session Management ---
59
62
  router.get('/sessions', async (req, res) => {
60
63
  try {
@@ -0,0 +1,9 @@
1
+ import { Router } from 'express';
2
+ import { SubagentRegistry } from '../../runtime/subagents/registry.js';
3
+ export function createAgentsRouter() {
4
+ const router = Router();
5
+ router.get('/metadata', (_req, res) => {
6
+ res.json({ agents: SubagentRegistry.getDisplayMetadata() });
7
+ });
8
+ return router;
9
+ }
@@ -3,8 +3,8 @@ import multer from 'multer';
3
3
  import path from 'path';
4
4
  import fs from 'fs-extra';
5
5
  import { homedir } from 'os';
6
- import { LinkRepository } from '../../runtime/link-repository.js';
7
- import { LinkWorker } from '../../runtime/link-worker.js';
6
+ import { LinkRepository } from '../../runtime/subagents/link/repository.js';
7
+ import { LinkWorker } from '../../runtime/subagents/link/worker.js';
8
8
  import { ConfigManager } from '../../config/manager.js';
9
9
  const DOCS_PATH = path.join(homedir(), '.morpheus', 'docs');
10
10
  // Configure multer for file uploads
@@ -62,6 +62,7 @@ export class ChronosRepository {
62
62
  addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0`, 'updated_at');
63
63
  addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN created_by TEXT NOT NULL DEFAULT 'api'`, 'created_by');
64
64
  addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN notify_channels TEXT NOT NULL DEFAULT '[]'`, 'notify_channels');
65
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN origin_session_id TEXT`, 'origin_session_id');
65
66
  const execInfo = this.db.pragma('table_info(chronos_executions)');
66
67
  const execCols = new Set(execInfo.map((c) => c.name));
67
68
  const addExecCol = (sql, col) => {
@@ -107,6 +108,7 @@ export class ChronosRepository {
107
108
  updated_at: row.updated_at,
108
109
  created_by: row.created_by,
109
110
  notify_channels,
111
+ origin_session_id: row.origin_session_id ?? null,
110
112
  };
111
113
  }
112
114
  deserializeExecution(row) {
@@ -132,9 +134,9 @@ export class ChronosRepository {
132
134
  this.db.prepare(`
133
135
  INSERT INTO chronos_jobs (
134
136
  id, prompt, schedule_type, schedule_expression, cron_normalized,
135
- timezone, next_run_at, last_run_at, enabled, created_at, updated_at, created_by, notify_channels
136
- ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 1, ?, ?, ?, ?)
137
- `).run(id, input.prompt, input.schedule_type, input.schedule_expression, input.cron_normalized ?? null, input.timezone, input.next_run_at, now, now, input.created_by, JSON.stringify(input.notify_channels ?? []));
137
+ timezone, next_run_at, last_run_at, enabled, created_at, updated_at, created_by, notify_channels, origin_session_id
138
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 1, ?, ?, ?, ?, ?)
139
+ `).run(id, input.prompt, input.schedule_type, input.schedule_expression, input.cron_normalized ?? null, input.timezone, input.next_run_at, now, now, input.created_by, JSON.stringify(input.notify_channels ?? []), input.origin_session_id ?? null);
138
140
  return this.getJob(id);
139
141
  }
140
142
  getJob(id) {
@@ -4,6 +4,7 @@ import { DisplayManager } from '../display.js';
4
4
  import { parseNextRun } from './parser.js';
5
5
  import { ChannelRegistry } from '../../channels/registry.js';
6
6
  import { AuditRepository } from '../audit/repository.js';
7
+ import { SQLiteChatMessageHistory } from '../memory/sqlite.js';
7
8
  export class ChronosWorker {
8
9
  repo;
9
10
  oracle;
@@ -74,9 +75,27 @@ export class ChronosWorker {
74
75
  const display = DisplayManager.getInstance();
75
76
  const execId = randomUUID();
76
77
  display.log(`Job ${job.id} triggered — "${job.prompt.slice(0, 60)}"`, { source: 'Chronos' });
77
- // Use the currently active Oracle session so Chronos executes in the
78
- // user's conversation context no session switching or isolation needed.
79
- const activeSessionId = this.oracle.getCurrentSessionId() ?? 'default';
78
+ // Resolve session: prefer the session where the job was originally created,
79
+ // fall back to Oracle's current session, then to most recent active session.
80
+ const tmpHistory = new SQLiteChatMessageHistory({ sessionId: 'tmp' });
81
+ let activeSessionId;
82
+ try {
83
+ if (job.origin_session_id && tmpHistory.isSessionUsable(job.origin_session_id)) {
84
+ activeSessionId = job.origin_session_id;
85
+ }
86
+ else {
87
+ const oracleSession = this.oracle.getCurrentSessionId();
88
+ if (oracleSession && tmpHistory.isSessionUsable(oracleSession)) {
89
+ activeSessionId = oracleSession;
90
+ }
91
+ else {
92
+ activeSessionId = tmpHistory.getMostRecentSession() ?? 'default';
93
+ }
94
+ }
95
+ }
96
+ finally {
97
+ tmpHistory.close();
98
+ }
80
99
  this.repo.insertExecution({
81
100
  id: execId,
82
101
  job_id: job.id,
@@ -96,7 +115,7 @@ export class ChronosWorker {
96
115
  const taskOriginChannel = job.notify_channels.length === 1
97
116
  ? job.notify_channels[0]
98
117
  : 'chronos';
99
- const taskContext = { origin_channel: taskOriginChannel, session_id: activeSessionId };
118
+ const taskContext = { origin_channel: taskOriginChannel, session_id: activeSessionId, source: 'chronos' };
100
119
  // Hard-block Chronos management tools during execution.
101
120
  ChronosWorker.isExecuting = true;
102
121
  const chronosStartMs = Date.now();
@@ -154,6 +173,10 @@ export class ChronosWorker {
154
173
  }
155
174
  else {
156
175
  for (const ch of job.notify_channels) {
176
+ // 'ui' has no adapter — it works by polling session messages from the DB.
177
+ // The response is already persisted via oracle.chat(), so skip silently.
178
+ if (ch === 'ui')
179
+ continue;
157
180
  const adapter = ChannelRegistry.get(ch);
158
181
  if (adapter) {
159
182
  await adapter.sendMessage(text).catch((err) => {
@@ -30,6 +30,7 @@ function makeJob(overrides = {}) {
30
30
  updated_at: Date.now() - 5000,
31
31
  created_by: 'ui',
32
32
  notify_channels: [],
33
+ origin_session_id: null,
33
34
  ...overrides,
34
35
  };
35
36
  }
@@ -91,7 +92,7 @@ describe('ChronosWorker.tick()', () => {
91
92
  await worker.tick();
92
93
  // Wait for fire-and-forget
93
94
  await new Promise((r) => setTimeout(r, 50));
94
- expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt));
95
+ expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt), undefined, false, expect.objectContaining({ source: 'chronos' }));
95
96
  expect(repo.disableJob).toHaveBeenCalledWith(job.id);
96
97
  expect(repo.updateJob).not.toHaveBeenCalled();
97
98
  });
@@ -102,7 +103,7 @@ describe('ChronosWorker.tick()', () => {
102
103
  const worker = new ChronosWorker(repo, oracle);
103
104
  await worker.tick();
104
105
  await new Promise((r) => setTimeout(r, 50));
105
- expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt));
106
+ expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt), undefined, false, expect.objectContaining({ source: 'chronos' }));
106
107
  expect(repo.disableJob).not.toHaveBeenCalled();
107
108
  expect(repo.updateJob).toHaveBeenCalledWith(job.id, expect.objectContaining({ next_run_at: expect.any(Number) }));
108
109
  });
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { ConfigManager } from '../config/manager.js';
9
9
  import { DisplayManager } from './display.js';
10
- import { Apoc } from './apoc.js';
11
- import { Neo } from './neo.js';
12
- import { Trinity } from './trinity.js';
10
+ import { Apoc } from './subagents/apoc.js';
11
+ import { Neo } from './subagents/neo.js';
12
+ import { Trinity } from './subagents/trinity/trinity.js';
13
13
  let currentOracle = null;
14
14
  /**
15
15
  * Register the current Oracle instance for hot-reload.
@@ -847,6 +847,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
847
847
  embedding_status = 'pending'
848
848
  WHERE id = ?
849
849
  `).run(now, now, sessionId);
850
+ // Remove channel/user bindings so channels don't route to an archived session
851
+ this.db.prepare(`
852
+ DELETE FROM user_channel_sessions WHERE session_id = ?
853
+ `).run(sessionId);
850
854
  // Exportar mensagens → texto
851
855
  const messages = this.db.prepare(`
852
856
  SELECT type, content
@@ -936,6 +940,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
936
940
  deleted_at = ?
937
941
  WHERE id = ?
938
942
  `).run(now, sessionId);
943
+ // Remove channel/user bindings so channels don't route to a deleted session
944
+ this.db.prepare(`
945
+ DELETE FROM user_channel_sessions WHERE session_id = ?
946
+ `).run(sessionId);
939
947
  });
940
948
  tx(); // Executar a transação
941
949
  }
@@ -977,6 +985,14 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
977
985
  this.db.prepare("INSERT INTO sessions (id, started_at, status) VALUES (?, ?, 'active')").run(sessionId, Date.now());
978
986
  }
979
987
  }
988
+ /**
989
+ * Checks whether a session exists and is usable (active or paused).
990
+ * Returns false for deleted, archived, or non-existent sessions.
991
+ */
992
+ isSessionUsable(sessionId) {
993
+ const row = this.db.prepare("SELECT status FROM sessions WHERE id = ?").get(sessionId);
994
+ return !!row && (row.status === 'active' || row.status === 'paused');
995
+ }
980
996
  /**
981
997
  * Validates that the target session exists and is usable (not archived/deleted).
982
998
  * No longer swaps active↔paused — sessions are independently usable from any channel.
@@ -1015,10 +1031,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
1015
1031
  */
1016
1032
  async listSessions() {
1017
1033
  const sessions = this.db.prepare(`
1018
- SELECT id, title, status, started_at
1019
- FROM sessions
1020
- WHERE status IN ('active', 'paused')
1021
- ORDER BY started_at DESC
1034
+ SELECT s.id, s.title, s.status, s.started_at,
1035
+ (SELECT MAX(m.created_at) FROM messages m WHERE m.session_id = s.id) AS last_message_at
1036
+ FROM sessions s
1037
+ WHERE s.status IN ('active', 'paused')
1038
+ ORDER BY COALESCE(last_message_at, s.started_at) DESC
1022
1039
  `).all();
1023
1040
  return sessions;
1024
1041
  }
@@ -1028,8 +1045,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
1028
1045
  */
1029
1046
  async getUserChannelSession(channel, userId) {
1030
1047
  const result = this.db.prepare(`
1031
- SELECT session_id FROM user_channel_sessions
1032
- WHERE channel = ? AND user_id = ?
1048
+ SELECT ucs.session_id
1049
+ FROM user_channel_sessions ucs
1050
+ JOIN sessions s ON s.id = ucs.session_id
1051
+ WHERE ucs.channel = ? AND ucs.user_id = ?
1052
+ AND s.status IN ('active', 'paused')
1033
1053
  `).get(channel, userId);
1034
1054
  return result ? result.session_id : null;
1035
1055
  }
@@ -1052,8 +1072,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
1052
1072
  */
1053
1073
  async listUserChannelSessions(channel) {
1054
1074
  const rows = this.db.prepare(`
1055
- SELECT user_id, session_id FROM user_channel_sessions
1056
- WHERE channel = ?
1075
+ SELECT ucs.user_id, ucs.session_id
1076
+ FROM user_channel_sessions ucs
1077
+ JOIN sessions s ON s.id = ucs.session_id
1078
+ WHERE ucs.channel = ?
1079
+ AND s.status IN ('active', 'paused')
1057
1080
  `).all(channel);
1058
1081
  return rows.map(row => ({ userId: row.user_id, sessionId: row.session_id }));
1059
1082
  }
@@ -1075,9 +1098,12 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
1075
1098
  return null;
1076
1099
  const placeholders = channels.map(() => '?').join(', ');
1077
1100
  const result = this.db.prepare(`
1078
- SELECT session_id FROM user_channel_sessions
1079
- WHERE channel IN (${placeholders})
1080
- ORDER BY updated_at DESC LIMIT 1
1101
+ SELECT ucs.session_id
1102
+ FROM user_channel_sessions ucs
1103
+ JOIN sessions s ON s.id = ucs.session_id
1104
+ WHERE ucs.channel IN (${placeholders})
1105
+ AND s.status IN ('active', 'paused')
1106
+ ORDER BY ucs.updated_at DESC LIMIT 1
1081
1107
  `).get(...channels);
1082
1108
  return result ? result.session_id : null;
1083
1109
  }
@@ -5,12 +5,9 @@ import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
7
7
  import { SatiMemoryMiddleware } from "./memory/sati/index.js";
8
- import { Apoc } from "./apoc.js";
8
+ import { Apoc, Neo, Trinity, Link, SubagentRegistry, emitToolAuditEvents } from "./subagents/index.js";
9
9
  import { TaskRequestContext } from "./tasks/context.js";
10
10
  import { TaskRepository } from "./tasks/repository.js";
11
- import { Neo } from "./neo.js";
12
- import { Trinity } from "./trinity.js";
13
- import { Link } from "./link.js";
14
11
  import { SmithDelegateTool } from "./tools/smith-tool.js";
15
12
  import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
16
13
  import { Construtor } from "./tools/factory.js";
@@ -20,12 +17,9 @@ import { SmithRegistry } from "./smiths/registry.js";
20
17
  import { AuditRepository } from "./audit/repository.js";
21
18
  import { SetupRepository } from './setup/repository.js';
22
19
  import { buildSetupTool } from './tools/setup-tool.js';
23
- import { emitToolAuditEvents } from "./subagent-utils.js";
20
+ import { SmithDelegator } from "./smiths/delegator.js";
24
21
  import { PATHS } from "../config/paths.js";
25
22
  import { writeFileSync } from "fs";
26
- const ORACLE_DELEGATION_TOOLS = new Set([
27
- 'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate', 'link_delegate',
28
- ]);
29
23
  export class Oracle {
30
24
  provider;
31
25
  config;
@@ -40,6 +34,48 @@ export class Oracle {
40
34
  this.config = config || ConfigManager.getInstance().get();
41
35
  this.databasePath = overrides?.databasePath;
42
36
  }
37
+ /**
38
+ * Registers Smith in the SubagentRegistry if Smiths are configured and enabled.
39
+ * Smith is special — it uses a standalone tool (SmithDelegateTool) and SmithDelegator
40
+ * rather than implementing ISubagent, so we create a minimal registration.
41
+ */
42
+ registerSmithIfEnabled() {
43
+ const smithsConfig = ConfigManager.getInstance().getSmithsConfig();
44
+ if (!smithsConfig.enabled || smithsConfig.entries.length === 0)
45
+ return;
46
+ if (SubagentRegistry.get('smith'))
47
+ return; // already registered
48
+ const delegator = SmithDelegator.getInstance();
49
+ SubagentRegistry.register({
50
+ agentKey: 'smith', auditAgent: 'smith', label: 'Smith',
51
+ delegateToolName: 'smith_delegate', emoji: '🕶️', color: 'gray',
52
+ description: 'Remote DevKit execution',
53
+ colorClass: 'text-gray-500 dark:text-gray-400',
54
+ bgClass: 'bg-gray-50 dark:bg-zinc-900',
55
+ badgeClass: 'bg-gray-200 text-gray-700 dark:bg-gray-700/60 dark:text-gray-300',
56
+ instance: {
57
+ initialize: async () => { },
58
+ execute: async (task, context) => delegator.delegate('unknown', task, context),
59
+ reload: async () => { },
60
+ createDelegateTool: () => SmithDelegateTool,
61
+ },
62
+ hasDynamicDescription: false,
63
+ isMultiInstance: true,
64
+ executeTask: async (task) => {
65
+ let smithName = 'unknown';
66
+ if (task.context) {
67
+ try {
68
+ const parsed = JSON.parse(task.context);
69
+ smithName = parsed.smith_name || parsed.smith || 'unknown';
70
+ }
71
+ catch {
72
+ smithName = task.context;
73
+ }
74
+ }
75
+ return delegator.delegate(smithName, task.input, task.context ?? undefined);
76
+ },
77
+ });
78
+ }
43
79
  buildDelegationFailureResponse() {
44
80
  return "Task enqueue could not be confirmed in the database. No task was created. Please retry.";
45
81
  }
@@ -115,13 +151,16 @@ export class Oracle {
115
151
  return valid;
116
152
  }
117
153
  hasDelegationToolCall(messages) {
154
+ const delegationTools = SubagentRegistry.getDelegationToolNames();
155
+ // Also include smith_delegate which may not be in registry if smiths are disabled
156
+ delegationTools.add('smith_delegate');
118
157
  for (const msg of messages) {
119
158
  if (!(msg instanceof AIMessage))
120
159
  continue;
121
160
  const toolCalls = msg.tool_calls ?? [];
122
161
  if (!Array.isArray(toolCalls))
123
162
  continue;
124
- if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate" || tc?.name === "smith_delegate" || tc?.name === "link_delegate")) {
163
+ if (toolCalls.some((tc) => delegationTools.has(tc?.name))) {
125
164
  return true;
126
165
  }
127
166
  }
@@ -152,27 +191,30 @@ export class Oracle {
152
191
  // Note: API Key validation is delegated to ProviderFactory or the Provider itself
153
192
  // to allow for Environment Variable fallback supported by LangChain.
154
193
  try {
155
- // Refresh Neo and Trinity tool catalogs so delegate descriptions contain runtime info.
156
- // Fail-open: Oracle can still initialize even if catalog refresh fails.
157
- await Neo.refreshDelegateCatalog().catch(() => { });
158
- await Trinity.refreshDelegateCatalog().catch(() => { });
159
- await Link.refreshDelegateCatalog().catch(() => { });
160
- // Build tool list conditionally include SmithDelegateTool based on config
194
+ // Ensure subagents are instantiated and self-registered before using the registry.
195
+ Apoc.getInstance();
196
+ Neo.getInstance();
197
+ Trinity.getInstance();
198
+ Link.getInstance();
199
+ // Register Smith in the registry if configured
200
+ this.registerSmithIfEnabled();
201
+ // Refresh dynamic tool catalogs so delegate descriptions contain runtime info.
202
+ await SubagentRegistry.refreshAllCatalogs();
161
203
  // Initialize setup repository (creates table if needed)
162
204
  SetupRepository.getInstance();
163
205
  const coreTools = [
164
206
  buildSetupTool(),
165
207
  TaskQueryTool,
166
- Neo.getInstance().createDelegateTool(),
167
- Apoc.getInstance().createDelegateTool(),
168
- Trinity.getInstance().createDelegateTool(),
169
- Link.getInstance().createDelegateTool(),
208
+ ...SubagentRegistry.getDelegationTools(),
170
209
  createLoadSkillTool(),
171
210
  timeVerifierTool,
172
211
  ...chronosTools,
173
212
  ];
213
+ // Smith's tool is already included via SubagentRegistry.getDelegationTools()
214
+ // if registerSmithIfEnabled() registered it. Only add it if Smith is enabled
215
+ // but NOT yet in the registry (shouldn't happen, but defensive).
174
216
  const smithsConfig = ConfigManager.getInstance().getSmithsConfig();
175
- if (smithsConfig.enabled && smithsConfig.entries.length > 0) {
217
+ if (smithsConfig.enabled && smithsConfig.entries.length > 0 && !SubagentRegistry.get('smith')) {
176
218
  coreTools.push(SmithDelegateTool);
177
219
  }
178
220
  this.provider = await ProviderFactory.create(this.config.llm, coreTools);
@@ -225,12 +267,14 @@ export class Oracle {
225
267
  provider: isTelephonist ? this.config.audio?.provider : this.config.llm.provider,
226
268
  model: isTelephonist ? this.config.audio?.model : this.config.llm.model
227
269
  };
228
- // Inject source metadata for automated origins (webhook, chronos)
229
- if (taskContext?.origin_channel === 'webhook') {
230
- userMessage.source_metadata = { source: 'webhook' };
231
- }
232
- else if (taskContext?.origin_channel === 'chronos') {
233
- userMessage.source_metadata = { source: 'chronos' };
270
+ // Inject source metadata for automated origins (webhook, chronos).
271
+ // Prefer explicit taskContext.source (set by Chronos even when origin_channel
272
+ // points to a specific notification channel like 'telegram').
273
+ const messageSource = taskContext?.source ?? (taskContext?.origin_channel === 'webhook' ? 'webhook'
274
+ : taskContext?.origin_channel === 'chronos' ? 'chronos'
275
+ : null);
276
+ if (messageSource) {
277
+ userMessage.source_metadata = { source: messageSource };
234
278
  }
235
279
  // Attach extra usage (e.g. from Audio) to the user message to be persisted
236
280
  if (extraUsage) {
@@ -465,10 +509,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
465
509
  }
466
510
  messages.push(...previousMessages);
467
511
  messages.push(userMessage);
468
- Apoc.setSessionId(currentSessionId);
469
- Neo.setSessionId(currentSessionId);
470
- Trinity.setSessionId(currentSessionId);
471
- Link.setSessionId(currentSessionId);
512
+ SubagentRegistry.setAllSessionIds(currentSessionId);
472
513
  const invokeContext = {
473
514
  origin_channel: taskContext?.origin_channel ?? "api",
474
515
  session_id: taskContext?.session_id ?? currentSessionId ?? "default",
@@ -514,8 +555,10 @@ Use it to inform your response and tool selection (if needed), but do not assume
514
555
  // Emit tool_call audit events for Oracle's independent tool calls.
515
556
  // Delegation tools (apoc/neo/trinity/smith/skill/link) are already audited
516
557
  // inside buildDelegationTool or the task system — skip them here.
558
+ const delegationToolNames = SubagentRegistry.getDelegationToolNames();
559
+ delegationToolNames.add('smith_delegate');
517
560
  emitToolAuditEvents(newGeneratedMessages, currentSessionId ?? 'default', 'oracle', {
518
- skipTools: ORACLE_DELEGATION_TOOLS,
561
+ skipTools: delegationToolNames,
519
562
  });
520
563
  // Inject provider/model metadata and duration into all new AI messages
521
564
  for (const msg of newGeneratedMessages) {
@@ -694,24 +737,16 @@ Use it to inform your response and tool selection (if needed), but do not assume
694
737
  }
695
738
  // Reload MCP tool cache from servers (slow path)
696
739
  await Construtor.reload();
697
- await Neo.refreshDelegateCatalog().catch(() => { });
698
- await Trinity.refreshDelegateCatalog().catch(() => { });
699
- await Link.refreshDelegateCatalog().catch(() => { });
740
+ await SubagentRegistry.refreshAllCatalogs();
700
741
  this.provider = await ProviderFactory.create(this.config.llm, [
701
742
  buildSetupTool(),
702
743
  TaskQueryTool,
703
- Neo.getInstance().createDelegateTool(),
704
- Apoc.getInstance().createDelegateTool(),
705
- Trinity.getInstance().createDelegateTool(),
706
- Link.getInstance().createDelegateTool(),
744
+ ...SubagentRegistry.getDelegationTools(),
707
745
  createLoadSkillTool(),
708
746
  timeVerifierTool,
709
747
  ...chronosTools,
710
748
  ]);
711
- await Neo.getInstance().reload();
712
- await Apoc.getInstance().reload();
713
- await Trinity.getInstance().reload();
714
- await Link.getInstance().reload();
715
- this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
749
+ await SubagentRegistry.reloadAll();
750
+ this.display.log(`Oracle and subagent tools reloaded`, { source: 'Oracle' });
716
751
  }
717
752
  }
@@ -1,12 +1,13 @@
1
1
  import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
- import { ConfigManager } from "../config/manager.js";
3
- import { ProviderFactory } from "./providers/factory.js";
4
- import { ProviderError } from "./errors.js";
5
- import { DisplayManager } from "./display.js";
2
+ import { ConfigManager } from "../../config/manager.js";
3
+ import { ProviderFactory } from "../providers/factory.js";
4
+ import { ProviderError } from "../errors.js";
5
+ import { DisplayManager } from "../display.js";
6
6
  import { buildDevKit } from "morpheus-devkit";
7
7
  import { instrumentDevKitTools } from "./devkit-instrument.js";
8
- import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
9
- import { buildDelegationTool } from "./tools/delegation-utils.js";
8
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./utils.js";
9
+ import { buildDelegationTool } from "../tools/delegation-utils.js";
10
+ import { SubagentRegistry } from "./registry.js";
10
11
  /**
11
12
  * Apoc is a subagent of Oracle specialized in devtools operations.
12
13
  * It receives delegated tasks from Oracle and executes them using DevKit tools
@@ -36,6 +37,18 @@ export class Apoc {
36
37
  static getInstance(config) {
37
38
  if (!Apoc.instance) {
38
39
  Apoc.instance = new Apoc(config);
40
+ SubagentRegistry.register({
41
+ agentKey: 'apoc', auditAgent: 'apoc', label: 'Apoc',
42
+ delegateToolName: 'apoc_delegate', emoji: '🧑‍🔬', color: 'amber',
43
+ description: 'Filesystem, shell & browser',
44
+ colorClass: 'text-amber-600 dark:text-amber-400',
45
+ bgClass: 'bg-amber-50 dark:bg-amber-900/10',
46
+ badgeClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
47
+ instance: Apoc.instance,
48
+ hasDynamicDescription: false,
49
+ isMultiInstance: false,
50
+ setSessionId: (id) => Apoc.setSessionId(id),
51
+ });
39
52
  }
40
53
  return Apoc.instance;
41
54
  }
@@ -1,4 +1,4 @@
1
- import { AuditRepository } from './audit/repository.js';
1
+ import { AuditRepository } from '../audit/repository.js';
2
2
  /**
3
3
  * Wraps a StructuredTool to record audit events on each invocation.
4
4
  * The `getSessionId` getter is called at invocation time so it reflects
@@ -0,0 +1,12 @@
1
+ // Re-exports for convenient external access
2
+ export { Apoc } from './apoc.js';
3
+ export { Neo } from './neo.js';
4
+ export { Trinity } from './trinity/trinity.js';
5
+ export { Link } from './link/link.js';
6
+ export { SubagentRegistry, SYSTEM_AGENTS } from './registry.js';
7
+ export { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from './utils.js';
8
+ export { LinkRepository } from './link/repository.js';
9
+ export { LinkWorker } from './link/worker.js';
10
+ export { LinkSearch } from './link/search.js';
11
+ export { instrumentDevKitTools } from './devkit-instrument.js';
12
+ export { testConnection, introspectSchema, executeQuery } from './trinity/connector.js';