morpheus-cli 0.8.5 → 0.8.7

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 (78) hide show
  1. package/dist/channels/telegram.js +43 -0
  2. package/dist/config/manager.js +3 -1
  3. package/dist/config/schemas.js +2 -0
  4. package/dist/http/api.js +70 -13
  5. package/dist/http/routers/chronos.js +12 -1
  6. package/dist/http/webhooks-router.js +11 -3
  7. package/dist/runtime/ISubagent.js +1 -0
  8. package/dist/runtime/apoc.js +49 -39
  9. package/dist/runtime/audit/repository.js +193 -6
  10. package/dist/runtime/chronos/repository.js +35 -0
  11. package/dist/runtime/keymaker.js +6 -30
  12. package/dist/runtime/memory/sati/index.js +22 -1
  13. package/dist/runtime/memory/sati/repository.js +39 -1
  14. package/dist/runtime/memory/sqlite.js +16 -3
  15. package/dist/runtime/neo.js +78 -34
  16. package/dist/runtime/oracle.js +68 -19
  17. package/dist/runtime/skills/tool.js +25 -0
  18. package/dist/runtime/subagent-utils.js +89 -0
  19. package/dist/runtime/tasks/repository.js +51 -0
  20. package/dist/runtime/tasks/worker.js +12 -2
  21. package/dist/runtime/telephonist.js +17 -9
  22. package/dist/runtime/tools/delegation-utils.js +120 -0
  23. package/dist/runtime/tools/index.js +0 -2
  24. package/dist/runtime/tools/time-verify-tools.js +15 -8
  25. package/dist/runtime/trinity.js +50 -34
  26. package/dist/runtime/webhooks/repository.js +31 -0
  27. package/dist/types/config.js +2 -0
  28. package/dist/types/pagination.js +1 -0
  29. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
  30. package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
  31. package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
  32. package/dist/ui/assets/{ConfirmationModal-MyIaIK_Z.js → ConfirmationModal-fvgnOWTY.js} +1 -1
  33. package/dist/ui/assets/{Dashboard-C52jjru9.js → Dashboard-Ca5mSefz.js} +1 -1
  34. package/dist/ui/assets/{DeleteConfirmationModal-B0nDocEK.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
  35. package/dist/ui/assets/{Logs-fDrGC9Lq.js → Logs-CYu7se7R.js} +1 -1
  36. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
  37. package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
  38. package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
  39. package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
  40. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
  41. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
  42. package/dist/ui/assets/{Settings-Cgd4dJdc.js → Settings-OQlHAJoy.js} +6 -4
  43. package/dist/ui/assets/Skills-Crsybug0.js +7 -0
  44. package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
  45. package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
  46. package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
  47. package/dist/ui/assets/{UsageStats-EEwfbJ6C.js → UsageStats-CBo2vW2n.js} +1 -1
  48. package/dist/ui/assets/{WebhookManager-CyVUcscY.js → WebhookManager-0tDFkfHd.js} +1 -1
  49. package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
  50. package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
  51. package/dist/ui/assets/{config-cslLZS3q.js → config-DteVgNGR.js} +1 -1
  52. package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
  53. package/dist/ui/assets/index-DcfyUdLI.css +1 -0
  54. package/dist/ui/assets/{mcp-M0iDC0mj.js → mcp-DxzodOdH.js} +1 -1
  55. package/dist/ui/assets/{skills-BvaaqiOT.js → skills--hAyQnmG.js} +1 -1
  56. package/dist/ui/assets/{stats-DALk3GOj.js → stats-Cibaisqd.js} +1 -1
  57. package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
  58. package/dist/ui/index.html +3 -3
  59. package/dist/ui/sw.js +1 -1
  60. package/package.json +2 -1
  61. package/dist/runtime/tools/apoc-tool.js +0 -157
  62. package/dist/runtime/tools/neo-tool.js +0 -172
  63. package/dist/runtime/tools/trinity-tool.js +0 -157
  64. package/dist/ui/assets/Chat-Cx2OgATp.js +0 -38
  65. package/dist/ui/assets/Chronos--mut48fM.js +0 -1
  66. package/dist/ui/assets/MCPManager-CtRQzwM8.js +0 -1
  67. package/dist/ui/assets/ModelPricing-d4EYrGko.js +0 -1
  68. package/dist/ui/assets/Notifications-Dkqug57C.js +0 -1
  69. package/dist/ui/assets/SatiMemories-DykYVHgi.js +0 -1
  70. package/dist/ui/assets/SessionAudit-Bk0-DpW0.js +0 -9
  71. package/dist/ui/assets/Skills-DSi313oC.js +0 -7
  72. package/dist/ui/assets/Smiths-DLys0BWT.js +0 -1
  73. package/dist/ui/assets/Tasks-B1MbPNUQ.js +0 -1
  74. package/dist/ui/assets/TrinityDatabases-B5SeHOLt.js +0 -1
  75. package/dist/ui/assets/chronos-BVRpP__j.js +0 -1
  76. package/dist/ui/assets/index-CpVvCthh.js +0 -10
  77. package/dist/ui/assets/index-QQyZIsmH.css +0 -1
  78. package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
@@ -65,7 +65,24 @@ export class AuditRepository {
65
65
  const rows = this.db.prepare(`
66
66
  SELECT ae.*,
67
67
  CASE
68
- WHEN ae.provider IS NOT NULL AND ae.model IS NOT NULL AND ae.input_tokens IS NOT NULL
68
+ -- Telephonist: prefer audio_cost_per_second when set
69
+ WHEN ae.event_type = 'telephonist'
70
+ AND mp.audio_cost_per_second IS NOT NULL
71
+ AND mp.audio_cost_per_second > 0
72
+ THEN
73
+ COALESCE(CAST(json_extract(ae.metadata, '$.audio_duration_seconds') AS REAL), 0)
74
+ * mp.audio_cost_per_second
75
+ -- Telephonist with token-based pricing (e.g. Gemini/OpenRouter with real tokens)
76
+ WHEN ae.event_type = 'telephonist'
77
+ AND ae.provider IS NOT NULL AND ae.model IS NOT NULL
78
+ AND ae.input_tokens IS NOT NULL AND ae.input_tokens > 0
79
+ THEN (
80
+ COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0) +
81
+ COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
82
+ )
83
+ -- All other events: token-based
84
+ WHEN ae.event_type != 'telephonist'
85
+ AND ae.provider IS NOT NULL AND ae.model IS NOT NULL AND ae.input_tokens IS NOT NULL
69
86
  THEN (
70
87
  COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0) +
71
88
  COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
@@ -84,11 +101,16 @@ export class AuditRepository {
84
101
  const events = this.getBySession(sessionId, { limit: 10_000 });
85
102
  const llmEvents = events.filter(e => e.event_type === 'llm_call');
86
103
  const toolEvents = events.filter(e => e.event_type === 'tool_call' || e.event_type === 'mcp_tool');
87
- const totalCostUsd = llmEvents.reduce((sum, e) => sum + (e.estimated_cost_usd ?? 0), 0);
104
+ const telephonistEvents = events.filter(e => e.event_type === 'telephonist');
105
+ const totalCostUsd = [...llmEvents, ...telephonistEvents].reduce((sum, e) => sum + (e.estimated_cost_usd ?? 0), 0);
88
106
  const totalDurationMs = events.reduce((sum, e) => sum + (e.duration_ms ?? 0), 0);
89
- // By agent
107
+ const totalAudioSeconds = telephonistEvents.reduce((sum, e) => {
108
+ const meta = e.metadata ? JSON.parse(e.metadata) : null;
109
+ return sum + (meta?.audio_duration_seconds ?? 0);
110
+ }, 0);
111
+ // By agent (llm + telephonist)
90
112
  const agentMap = new Map();
91
- for (const e of llmEvents) {
113
+ for (const e of [...llmEvents, ...telephonistEvents]) {
92
114
  const key = e.agent ?? 'unknown';
93
115
  const existing = agentMap.get(key) ?? { llmCalls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 };
94
116
  agentMap.set(key, {
@@ -98,9 +120,9 @@ export class AuditRepository {
98
120
  estimatedCostUsd: existing.estimatedCostUsd + (e.estimated_cost_usd ?? 0),
99
121
  });
100
122
  }
101
- // By model
123
+ // By model (llm + telephonist)
102
124
  const modelMap = new Map();
103
- for (const e of llmEvents) {
125
+ for (const e of [...llmEvents, ...telephonistEvents]) {
104
126
  if (!e.model)
105
127
  continue;
106
128
  const key = `${e.provider}/${e.model}`;
@@ -116,6 +138,7 @@ export class AuditRepository {
116
138
  return {
117
139
  totalCostUsd,
118
140
  totalDurationMs,
141
+ totalAudioSeconds,
119
142
  llmCallCount: llmEvents.length,
120
143
  toolCallCount: toolEvents.length,
121
144
  byAgent: Array.from(agentMap.entries()).map(([agent, s]) => ({ agent, ...s })),
@@ -129,4 +152,168 @@ export class AuditRepository {
129
152
  })),
130
153
  };
131
154
  }
155
+ getGlobalSummary() {
156
+ // Reusable cost expression (telephonist + token-based)
157
+ const costExpr = `
158
+ CASE
159
+ WHEN ae.event_type = 'telephonist'
160
+ AND mp.audio_cost_per_second IS NOT NULL AND mp.audio_cost_per_second > 0
161
+ THEN COALESCE(CAST(json_extract(ae.metadata, '$.audio_duration_seconds') AS REAL), 0)
162
+ * mp.audio_cost_per_second
163
+ WHEN ae.event_type = 'telephonist'
164
+ AND ae.input_tokens IS NOT NULL AND ae.input_tokens > 0
165
+ THEN COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0)
166
+ + COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
167
+ WHEN ae.event_type != 'telephonist'
168
+ AND ae.provider IS NOT NULL AND ae.input_tokens IS NOT NULL
169
+ THEN COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0)
170
+ + COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
171
+ ELSE 0
172
+ END`;
173
+ // ── Sessions ─────────────────────────────────────────────────────────────
174
+ const sessionsRow = this.db.prepare(`
175
+ SELECT
176
+ COUNT(*) as total,
177
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
178
+ SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
179
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
180
+ SUM(CASE WHEN status = 'deleted' THEN 1 ELSE 0 END) as deleted
181
+ FROM sessions
182
+ `).get();
183
+ const withAuditRow = this.db.prepare(`SELECT COUNT(DISTINCT session_id) as n FROM audit_events`).get();
184
+ // ── Global totals (single JOIN pass) ─────────────────────────────────────
185
+ const totalsRow = this.db.prepare(`
186
+ SELECT
187
+ COUNT(*) as totalEventCount,
188
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
189
+ SUM(CASE WHEN ae.event_type = 'tool_call' THEN 1 ELSE 0 END) as toolCallCount,
190
+ SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
191
+ SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
192
+ SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
193
+ SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,
194
+ SUM(CASE WHEN ae.event_type = 'task_created' THEN 1 ELSE 0 END) as taskCreatedCount,
195
+ SUM(CASE WHEN ae.event_type = 'task_completed' THEN 1 ELSE 0 END) as taskCompletedCount,
196
+ SUM(CASE WHEN ae.event_type = 'telephonist' THEN 1 ELSE 0 END) as telephonistCount,
197
+ COALESCE(SUM(ae.input_tokens), 0) as totalInputTokens,
198
+ COALESCE(SUM(ae.output_tokens), 0) as totalOutputTokens,
199
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
200
+ COALESCE(SUM(
201
+ CASE WHEN ae.event_type = 'telephonist'
202
+ THEN COALESCE(CAST(json_extract(ae.metadata,'$.audio_duration_seconds') AS REAL),0)
203
+ ELSE 0 END
204
+ ), 0) as totalAudioSeconds,
205
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
206
+ FROM audit_events ae
207
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
208
+ `).get();
209
+ // ── By agent ─────────────────────────────────────────────────────────────
210
+ const byAgentRows = this.db.prepare(`
211
+ SELECT
212
+ COALESCE(ae.agent, 'unknown') as agent,
213
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCalls,
214
+ SUM(CASE WHEN ae.event_type IN ('tool_call','mcp_tool') THEN 1 ELSE 0 END) as toolCalls,
215
+ COALESCE(SUM(ae.input_tokens), 0) as inputTokens,
216
+ COALESCE(SUM(ae.output_tokens), 0) as outputTokens,
217
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
218
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
219
+ FROM audit_events ae
220
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
221
+ GROUP BY ae.agent
222
+ ORDER BY estimatedCostUsd DESC
223
+ `).all();
224
+ // ── By model ─────────────────────────────────────────────────────────────
225
+ const byModelRows = this.db.prepare(`
226
+ SELECT
227
+ ae.provider,
228
+ ae.model,
229
+ COUNT(*) as calls,
230
+ COALESCE(SUM(ae.input_tokens), 0) as inputTokens,
231
+ COALESCE(SUM(ae.output_tokens), 0) as outputTokens,
232
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
233
+ FROM audit_events ae
234
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
235
+ WHERE ae.model IS NOT NULL
236
+ GROUP BY ae.provider, ae.model
237
+ ORDER BY estimatedCostUsd DESC
238
+ `).all();
239
+ // ── Top tools ─────────────────────────────────────────────────────────────
240
+ const topToolsRows = this.db.prepare(`
241
+ SELECT
242
+ ae.tool_name,
243
+ ae.agent,
244
+ ae.event_type,
245
+ COUNT(*) as count,
246
+ SUM(CASE WHEN ae.status = 'error' THEN 1 ELSE 0 END) as errorCount
247
+ FROM audit_events ae
248
+ WHERE ae.tool_name IS NOT NULL
249
+ AND ae.event_type IN ('tool_call','mcp_tool')
250
+ GROUP BY ae.tool_name, ae.agent, ae.event_type
251
+ ORDER BY count DESC
252
+ LIMIT 20
253
+ `).all();
254
+ // ── Recent sessions ──────────────────────────────────────────────────────
255
+ const recentRows = this.db.prepare(`
256
+ SELECT
257
+ ae.session_id,
258
+ s.title,
259
+ s.status,
260
+ s.started_at,
261
+ COUNT(ae.id) as event_count,
262
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
263
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
264
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
265
+ FROM audit_events ae
266
+ INNER JOIN sessions s ON ae.session_id = s.id
267
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
268
+ GROUP BY ae.session_id
269
+ ORDER BY MAX(ae.created_at) DESC
270
+ LIMIT 20
271
+ `).all();
272
+ // ── Daily activity (last 30 days) ─────────────────────────────────────────
273
+ const since = Date.now() - 30 * 24 * 60 * 60 * 1000;
274
+ const dailyRows = this.db.prepare(`
275
+ SELECT
276
+ date(ae.created_at / 1000, 'unixepoch') as date,
277
+ COUNT(*) as eventCount,
278
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
279
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
280
+ FROM audit_events ae
281
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
282
+ WHERE ae.created_at >= ?
283
+ GROUP BY date
284
+ ORDER BY date ASC
285
+ `).all(since);
286
+ return {
287
+ sessions: {
288
+ total: sessionsRow?.total ?? 0,
289
+ active: sessionsRow?.active ?? 0,
290
+ paused: sessionsRow?.paused ?? 0,
291
+ archived: sessionsRow?.archived ?? 0,
292
+ deleted: sessionsRow?.deleted ?? 0,
293
+ withAudit: withAuditRow?.n ?? 0,
294
+ },
295
+ totals: {
296
+ estimatedCostUsd: totalsRow?.estimatedCostUsd ?? 0,
297
+ totalDurationMs: totalsRow?.totalDurationMs ?? 0,
298
+ totalAudioSeconds: totalsRow?.totalAudioSeconds ?? 0,
299
+ totalInputTokens: totalsRow?.totalInputTokens ?? 0,
300
+ totalOutputTokens: totalsRow?.totalOutputTokens ?? 0,
301
+ totalEventCount: totalsRow?.totalEventCount ?? 0,
302
+ llmCallCount: totalsRow?.llmCallCount ?? 0,
303
+ toolCallCount: totalsRow?.toolCallCount ?? 0,
304
+ mcpToolCount: totalsRow?.mcpToolCount ?? 0,
305
+ skillCount: totalsRow?.skillCount ?? 0,
306
+ memoryRecoveryCount: totalsRow?.memoryRecoveryCount ?? 0,
307
+ chronosJobCount: totalsRow?.chronosJobCount ?? 0,
308
+ taskCreatedCount: totalsRow?.taskCreatedCount ?? 0,
309
+ taskCompletedCount: totalsRow?.taskCompletedCount ?? 0,
310
+ telephonistCount: totalsRow?.telephonistCount ?? 0,
311
+ },
312
+ byAgent: byAgentRows,
313
+ byModel: byModelRows,
314
+ topTools: topToolsRows,
315
+ recentSessions: recentRows,
316
+ dailyActivity: dailyRows,
317
+ };
318
+ }
132
319
  }
@@ -156,6 +156,41 @@ export class ChronosRepository {
156
156
  const rows = this.db.prepare(query).all(...params);
157
157
  return rows.map((r) => this.deserializeJob(r));
158
158
  }
159
+ countJobs(filters) {
160
+ const params = [];
161
+ let query = 'SELECT COUNT(*) as cnt FROM chronos_jobs WHERE 1=1';
162
+ if (filters?.enabled !== undefined) {
163
+ query += ' AND enabled = ?';
164
+ params.push(filters.enabled ? 1 : 0);
165
+ }
166
+ if (filters?.created_by) {
167
+ query += ' AND created_by = ?';
168
+ params.push(filters.created_by);
169
+ }
170
+ const row = this.db.prepare(query).get(...params);
171
+ return row.cnt;
172
+ }
173
+ listJobsPaginated(filters) {
174
+ const page = Math.max(1, filters?.page ?? 1);
175
+ const per_page = Math.min(100, Math.max(1, filters?.per_page ?? 20));
176
+ const offset = (page - 1) * per_page;
177
+ const total = this.countJobs(filters);
178
+ const total_pages = Math.ceil(total / per_page);
179
+ const params = [];
180
+ let query = 'SELECT * FROM chronos_jobs WHERE 1=1';
181
+ if (filters?.enabled !== undefined) {
182
+ query += ' AND enabled = ?';
183
+ params.push(filters.enabled ? 1 : 0);
184
+ }
185
+ if (filters?.created_by) {
186
+ query += ' AND created_by = ?';
187
+ params.push(filters.created_by);
188
+ }
189
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
190
+ params.push(per_page, offset);
191
+ const rows = this.db.prepare(query).all(...params);
192
+ return { data: rows.map((r) => this.deserializeJob(r)), total, page, per_page, total_pages };
193
+ }
159
194
  updateJob(id, patch) {
160
195
  const now = Date.now();
161
196
  const sets = ['updated_at = ?'];
@@ -8,7 +8,7 @@ import { Construtor } from "./tools/factory.js";
8
8
  import { morpheusTools } from "./tools/index.js";
9
9
  import { SkillRegistry } from "./skills/registry.js";
10
10
  import { TaskRequestContext } from "./tasks/context.js";
11
- import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
11
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
12
12
  /**
13
13
  * Keymaker is a specialized agent for executing skills.
14
14
  * "The one who opens any door" - has access to ALL tools:
@@ -122,38 +122,14 @@ CRITICAL — NEVER FABRICATE DATA:
122
122
  const content = typeof lastMessage.content === "string"
123
123
  ? lastMessage.content
124
124
  : JSON.stringify(lastMessage.content);
125
- // Persist message with token usage metadata (like Trinity/Neo/Apoc)
126
125
  const keymakerConfig = this.config.keymaker || this.config.llm;
127
126
  const targetSession = taskContext?.session_id ?? "keymaker";
128
- const rawUsage = lastMessage.usage_metadata
129
- ?? lastMessage.response_metadata?.usage
130
- ?? lastMessage.response_metadata?.tokenUsage
131
- ?? lastMessage.usage;
132
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
133
- try {
134
- const persisted = new AIMessage(content);
135
- if (rawUsage)
136
- persisted.usage_metadata = rawUsage;
137
- persisted.provider_metadata = { provider: keymakerConfig.provider, model: keymakerConfig.model };
138
- persisted.agent_metadata = { agent: 'keymaker' };
139
- persisted.duration_ms = durationMs;
140
- await history.addMessage(persisted);
141
- }
142
- finally {
143
- history.close();
144
- }
127
+ const rawUsage = extractRawUsage(lastMessage);
128
+ const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
129
+ await persistAgentMessage('keymaker', content, keymakerConfig, targetSession, rawUsage, durationMs);
130
+ emitToolAuditEvents(response.messages.slice(2), targetSession, 'keymaker');
145
131
  this.display.log(`Keymaker completed skill "${this.skillName}" execution`, { source: "Keymaker" });
146
- return {
147
- output: content,
148
- usage: {
149
- provider: keymakerConfig.provider,
150
- model: keymakerConfig.model,
151
- inputTokens: rawUsage?.input_tokens ?? 0,
152
- outputTokens: rawUsage?.output_tokens ?? 0,
153
- durationMs,
154
- stepCount: response.messages.filter((m) => m instanceof AIMessage).length,
155
- },
156
- };
132
+ return buildAgentResult(content, keymakerConfig, rawUsage, durationMs, stepCount);
157
133
  }
158
134
  catch (err) {
159
135
  this.display.log(`Keymaker execution error: ${err.message}`, { source: "Keymaker", level: "error" });
@@ -1,6 +1,7 @@
1
1
  import { AIMessage } from "@langchain/core/messages";
2
2
  import { SatiService } from "./service.js";
3
3
  import { DisplayManager } from "../../display.js";
4
+ import { AuditRepository } from "../../audit/repository.js";
4
5
  const display = DisplayManager.getInstance();
5
6
  export class SatiMemoryMiddleware {
6
7
  service;
@@ -14,12 +15,25 @@ export class SatiMemoryMiddleware {
14
15
  }
15
16
  return SatiMemoryMiddleware.instance;
16
17
  }
17
- async beforeAgent(currentMessage, history) {
18
+ async beforeAgent(currentMessage, history, sessionId) {
19
+ const startMs = Date.now();
18
20
  try {
19
21
  // Extract recent messages content strings for context
20
22
  const recentText = history.slice(-10).map(m => m.content.toString());
21
23
  display.log(`Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
22
24
  const result = await this.service.recover(currentMessage, recentText);
25
+ const durationMs = Date.now() - startMs;
26
+ AuditRepository.getInstance().insert({
27
+ session_id: sessionId ?? 'sati-recovery',
28
+ event_type: 'memory_recovery',
29
+ agent: 'sati',
30
+ duration_ms: durationMs,
31
+ status: 'success',
32
+ metadata: {
33
+ memories_count: result.relevant_memories.length,
34
+ memories: result.relevant_memories.map(m => ({ category: m.category, importance: m.importance, summary: m.summary })),
35
+ },
36
+ });
23
37
  if (result.relevant_memories.length === 0) {
24
38
  display.log('No relevant memories found', { source: 'Sati' });
25
39
  return null;
@@ -36,6 +50,13 @@ export class SatiMemoryMiddleware {
36
50
  `);
37
51
  }
38
52
  catch (error) {
53
+ AuditRepository.getInstance().insert({
54
+ session_id: sessionId ?? 'sati-recovery',
55
+ event_type: 'memory_recovery',
56
+ agent: 'sati',
57
+ duration_ms: Date.now() - startMs,
58
+ status: 'error',
59
+ });
39
60
  display.log(`Error in beforeAgent: ${error}`, { source: 'Sati' });
40
61
  // Fail open: return null so execution continues without memory
41
62
  return null;
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
5
5
  import { randomUUID } from 'crypto';
6
6
  import loadVecExtension from '../sqlite-vec.js';
7
7
  import { DisplayManager } from '../../display.js';
8
+ import { ConfigManager } from '../../../config/manager.js';
8
9
  const EMBEDDING_DIM = 384;
9
10
  export class SatiRepository {
10
11
  db = null;
@@ -208,7 +209,7 @@ export class SatiRepository {
208
209
  searchUnifiedVector(embedding, limit) {
209
210
  if (!this.db)
210
211
  return [];
211
- const SIMILARITY_THRESHOLD = 0.95;
212
+ const SIMILARITY_THRESHOLD = ConfigManager.getInstance().getSatiConfig().similarity_threshold ?? 0.9;
212
213
  const stmt = this.db.prepare(`
213
214
  SELECT *
214
215
  FROM (
@@ -412,6 +413,43 @@ export class SatiRepository {
412
413
  const rows = this.db.prepare('SELECT * FROM long_term_memory WHERE archived = 0 ORDER BY created_at DESC').all();
413
414
  return rows.map(this.mapRowToRecord);
414
415
  }
416
+ buildMemoryFilterQuery(filters) {
417
+ const params = [];
418
+ let where = 'WHERE archived = 0';
419
+ if (filters?.category) {
420
+ where += ' AND category = ?';
421
+ params.push(filters.category);
422
+ }
423
+ if (filters?.importance) {
424
+ where += ' AND importance = ?';
425
+ params.push(filters.importance);
426
+ }
427
+ if (filters?.search) {
428
+ where += ' AND (summary LIKE ? OR details LIKE ? OR category LIKE ?)';
429
+ const pattern = `%${filters.search}%`;
430
+ params.push(pattern, pattern, pattern);
431
+ }
432
+ return { where, params };
433
+ }
434
+ countMemories(filters) {
435
+ if (!this.db)
436
+ this.initialize();
437
+ const { where, params } = this.buildMemoryFilterQuery(filters);
438
+ const row = this.db.prepare(`SELECT COUNT(*) as cnt FROM long_term_memory ${where}`).get(...params);
439
+ return row.cnt;
440
+ }
441
+ getMemoriesPaginated(filters) {
442
+ if (!this.db)
443
+ this.initialize();
444
+ const page = Math.max(1, filters?.page ?? 1);
445
+ const per_page = Math.min(100, Math.max(1, filters?.per_page ?? 20));
446
+ const offset = (page - 1) * per_page;
447
+ const total = this.countMemories(filters);
448
+ const total_pages = Math.ceil(total / per_page);
449
+ const { where, params } = this.buildMemoryFilterQuery(filters);
450
+ const rows = this.db.prepare(`SELECT * FROM long_term_memory ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, per_page, offset);
451
+ return { data: rows.map(this.mapRowToRecord), total, page, per_page, total_pages };
452
+ }
415
453
  mapRowToRecord(row) {
416
454
  return {
417
455
  id: row.id,
@@ -204,6 +204,19 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
204
204
  catch (error) {
205
205
  console.warn(`[SQLite] Migration check failed: ${error}`);
206
206
  }
207
+ // Migrate model_pricing table
208
+ try {
209
+ const pricingInfo = this.db.pragma('table_info(model_pricing)');
210
+ const pricingCols = new Set(pricingInfo.map(c => c.name));
211
+ if (!pricingCols.has('audio_cost_per_second')) {
212
+ this.db.exec('ALTER TABLE model_pricing ADD COLUMN audio_cost_per_second REAL');
213
+ // Seed Whisper pricing: $0.006/minute = $0.0001/second
214
+ this.db.prepare('INSERT OR IGNORE INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second) VALUES (?, ?, ?, ?, ?)').run('openai', 'whisper-1', 0, 0, 0.0001);
215
+ }
216
+ }
217
+ catch (error) {
218
+ console.warn(`[SQLite] model_pricing migration failed: ${error}`);
219
+ }
207
220
  }
208
221
  /**
209
222
  * Retrieves all messages for the current session from the database.
@@ -301,7 +314,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
301
314
  }
302
315
  try {
303
316
  const placeholders = sessionIds.map(() => '?').join(', ');
304
- const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, agent, duration_ms
317
+ const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, agent, duration_ms, audio_duration_seconds
305
318
  FROM messages
306
319
  WHERE session_id IN (${placeholders})
307
320
  ORDER BY id DESC
@@ -648,11 +661,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
648
661
  }
649
662
  // --- Model Pricing CRUD ---
650
663
  listModelPricing() {
651
- const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m FROM model_pricing ORDER BY provider, model').all();
664
+ const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second FROM model_pricing ORDER BY provider, model').all();
652
665
  return rows;
653
666
  }
654
667
  upsertModelPricing(entry) {
655
- this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES (?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m);
668
+ this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second) VALUES (?, ?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m, audio_cost_per_second = excluded.audio_cost_per_second').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m, entry.audio_cost_per_second ?? null);
656
669
  }
657
670
  deleteModelPricing(provider, model) {
658
671
  const result = this.db.prepare('DELETE FROM model_pricing WHERE provider = ? AND model = ?').run(provider, model);
@@ -5,12 +5,50 @@ import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { Construtor } from "./tools/factory.js";
7
7
  import { morpheusTools } from "./tools/index.js";
8
- import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
9
8
  import { TaskRequestContext } from "./tasks/context.js";
10
- import { updateNeoDelegateToolDescription } from "./tools/neo-tool.js";
9
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
10
+ import { buildDelegationTool } from "./tools/delegation-utils.js";
11
+ // Internal Morpheus tools get 'tool_call' event type; MCP tools get 'mcp_tool'
12
+ const MORPHEUS_TOOL_NAMES = new Set(morpheusTools.map((t) => t.name));
13
+ const NEO_BUILTIN_CAPABILITIES = `
14
+ Neo built-in capabilities (always available — no MCP required):
15
+ • Config: morpheus_config_query, morpheus_config_update — read/write Morpheus configuration (LLM, channels, UI, etc.)
16
+ • Diagnostics: diagnostic_check — full system health report (config, databases, LLM provider, logs)
17
+ • Analytics: message_count, token_usage, provider_model_usage — message counts and token/cost usage stats
18
+ • Tasks: task_query — look up task status by id or session
19
+ • MCP Management: mcp_list, mcp_manage — list/add/update/delete/enable/disable MCP servers; use action "reload" to reload tools across all agents after config changes
20
+ • Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key`.trim();
21
+ const NEO_BASE_DESCRIPTION = `Delegate execution to Neo asynchronously.
22
+
23
+ This tool creates a background task and returns an acknowledgement with task id.
24
+ Use it for any request that requires Neo's built-in capabilities or a runtime MCP tool listed below.
25
+ Each delegated task must contain one atomic objective.
26
+
27
+ ${NEO_BUILTIN_CAPABILITIES}`;
28
+ function normalizeDescription(text) {
29
+ if (!text)
30
+ return "No description";
31
+ return text.replace(/\s+/g, " ").trim();
32
+ }
33
+ function buildCatalogSection(mcpTools) {
34
+ if (mcpTools.length === 0) {
35
+ return "\n\nRuntime MCP tools: none currently loaded.";
36
+ }
37
+ const maxItems = 500;
38
+ const lines = mcpTools.slice(0, maxItems).map((t) => {
39
+ const desc = normalizeDescription(t.description).slice(0, 120);
40
+ return `- ${t.name}: ${desc}`;
41
+ });
42
+ const hidden = mcpTools.length - lines.length;
43
+ if (hidden > 0) {
44
+ lines.push(`- ... and ${hidden} more tools`);
45
+ }
46
+ return `\n\nRuntime MCP tools:\n${lines.join("\n")}`;
47
+ }
11
48
  export class Neo {
12
49
  static instance = null;
13
50
  static currentSessionId = undefined;
51
+ static _delegateTool = null;
14
52
  agent;
15
53
  config;
16
54
  display = DisplayManager.getInstance();
@@ -28,17 +66,25 @@ export class Neo {
28
66
  }
29
67
  static resetInstance() {
30
68
  Neo.instance = null;
69
+ Neo._delegateTool = null;
31
70
  }
32
71
  static async refreshDelegateCatalog() {
33
72
  const mcpTools = await Construtor.create(() => Neo.currentSessionId);
34
- updateNeoDelegateToolDescription(mcpTools);
73
+ if (Neo._delegateTool) {
74
+ const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(mcpTools)}`;
75
+ Neo._delegateTool.description = full;
76
+ }
35
77
  }
36
78
  async initialize() {
37
79
  const neoConfig = this.config.neo || this.config.llm;
38
80
  const personality = this.config.neo?.personality || 'analytical_engineer';
39
81
  const mcpTools = await Construtor.create(() => Neo.currentSessionId);
40
82
  const tools = [...mcpTools, ...morpheusTools];
41
- updateNeoDelegateToolDescription(mcpTools);
83
+ // Update delegate tool description with current catalog
84
+ if (Neo._delegateTool) {
85
+ const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(mcpTools)}`;
86
+ Neo._delegateTool.description = full;
87
+ }
42
88
  this.display.log(`Neo initialized with ${tools.length} tools (personality: ${personality}).`, { source: "Neo" });
43
89
  try {
44
90
  this.agent = await ProviderFactory.create(neoConfig, tools);
@@ -89,6 +135,7 @@ ${context ? `Context:\n${context}` : ""}
89
135
  origin_message_id: taskContext?.origin_message_id,
90
136
  origin_user_id: taskContext?.origin_user_id,
91
137
  };
138
+ const inputCount = messages.length;
92
139
  const startMs = Date.now();
93
140
  const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }));
94
141
  const durationMs = Date.now() - startMs;
@@ -96,44 +143,41 @@ ${context ? `Context:\n${context}` : ""}
96
143
  const content = typeof lastMessage.content === "string"
97
144
  ? lastMessage.content
98
145
  : JSON.stringify(lastMessage.content);
99
- const rawUsage = lastMessage.usage_metadata
100
- ?? lastMessage.response_metadata?.usage
101
- ?? lastMessage.response_metadata?.tokenUsage
102
- ?? lastMessage.usage;
103
- const inputTokens = rawUsage?.input_tokens ?? 0;
104
- const outputTokens = rawUsage?.output_tokens ?? 0;
146
+ const rawUsage = extractRawUsage(lastMessage);
105
147
  const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
106
148
  const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
107
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
108
- try {
109
- const persisted = new AIMessage(content);
110
- if (rawUsage)
111
- persisted.usage_metadata = rawUsage;
112
- persisted.provider_metadata = { provider: neoConfig.provider, model: neoConfig.model };
113
- persisted.agent_metadata = { agent: 'neo' };
114
- persisted.duration_ms = durationMs;
115
- await history.addMessage(persisted);
116
- }
117
- finally {
118
- history.close();
119
- }
149
+ await persistAgentMessage('neo', content, neoConfig, targetSession, rawUsage, durationMs);
150
+ emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'neo', {
151
+ defaultEventType: 'mcp_tool',
152
+ internalToolNames: MORPHEUS_TOOL_NAMES,
153
+ });
120
154
  this.display.log("Neo task completed.", { source: "Neo" });
121
- return {
122
- output: content,
123
- usage: {
124
- provider: neoConfig.provider,
125
- model: neoConfig.model,
126
- inputTokens,
127
- outputTokens,
128
- durationMs,
129
- stepCount,
130
- },
131
- };
155
+ return buildAgentResult(content, neoConfig, rawUsage, durationMs, stepCount);
132
156
  }
133
157
  catch (err) {
134
158
  throw new ProviderError(neoConfig.provider, err, "Neo task execution failed");
135
159
  }
136
160
  }
161
+ createDelegateTool() {
162
+ if (!Neo._delegateTool) {
163
+ Neo._delegateTool = buildDelegationTool({
164
+ name: "neo_delegate",
165
+ description: NEO_BASE_DESCRIPTION,
166
+ agentKey: "neo",
167
+ agentLabel: "Neo",
168
+ auditAgent: "neo",
169
+ isSync: () => ConfigManager.getInstance().get().neo?.execution_mode === 'sync',
170
+ notifyText: '🥷 Neo is executing your request...',
171
+ executeSync: (task, context, sessionId, ctx) => Neo.getInstance().execute(task, context, sessionId, {
172
+ origin_channel: ctx?.origin_channel ?? "api",
173
+ session_id: sessionId,
174
+ origin_message_id: ctx?.origin_message_id,
175
+ origin_user_id: ctx?.origin_user_id,
176
+ }),
177
+ });
178
+ }
179
+ return Neo._delegateTool;
180
+ }
137
181
  async reload() {
138
182
  this.config = ConfigManager.getInstance().get();
139
183
  this.agent = undefined;