morpheus-cli 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/config/manager.js +11 -0
- package/dist/config/schemas.js +5 -0
- package/dist/http/api.js +3 -0
- package/dist/http/routers/danger.js +137 -0
- package/dist/runtime/audit/repository.js +2 -0
- package/dist/runtime/memory/sati/index.js +1 -1
- package/dist/runtime/memory/sati/service.js +27 -0
- package/dist/runtime/memory/session-embedding-worker.js +43 -36
- package/dist/runtime/oracle.js +25 -2
- package/dist/runtime/setup/__tests__/repository.test.js +115 -0
- package/dist/runtime/setup/repository.js +87 -0
- package/dist/runtime/tools/setup-tool.js +57 -0
- package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +1 -0
- package/dist/ui/assets/{Chat-ChsmnZzq.js → Chat-5AeRYuRj.js} +2 -2
- package/dist/ui/assets/{Chronos-kgO7IkEj.js → Chronos-BrKldYVw.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-D1BYPXJ4.js → ConfirmationModal-DsbS3XkJ.js} +1 -1
- package/dist/ui/assets/{Dashboard-DWB5NwQn.js → Dashboard-DvrTXLdo.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-CgIMbyB7.js → DeleteConfirmationModal-BfSjv04R.js} +1 -1
- package/dist/ui/assets/{Logs-DGdRnEFi.js → Logs-B0ZYWs5x.js} +1 -1
- package/dist/ui/assets/MCPManager-BwHGTeNs.js +1 -0
- package/dist/ui/assets/{ModelPricing-DAk1sS7D.js → ModelPricing-CYhGRQr8.js} +1 -1
- package/dist/ui/assets/{Notifications-DMEq6EZR.js → Notifications-BYMAtVMq.js} +1 -1
- package/dist/ui/assets/{Pagination-JsiwxVNQ.js → Pagination-oTGieBLM.js} +1 -1
- package/dist/ui/assets/SatiMemories-I1vsYtP2.js +1 -0
- package/dist/ui/assets/SessionAudit-BCecQWde.js +9 -0
- package/dist/ui/assets/Settings-Cu4D-7tb.js +47 -0
- package/dist/ui/assets/Skills-lGU3I5DO.js +7 -0
- package/dist/ui/assets/Smiths-DnEH3nID.js +1 -0
- package/dist/ui/assets/Tasks-Bz92GPWK.js +1 -0
- package/dist/ui/assets/{TrinityDatabases-BmM1S9aQ.js → TrinityDatabases-BUY-3j7Q.js} +1 -1
- package/dist/ui/assets/{UsageStats-aAu2DFlb.js → UsageStats-Dr5eSgJc.js} +1 -1
- package/dist/ui/assets/{WebhookManager-DdnRSWl9.js → WebhookManager-DIASAC-1.js} +1 -1
- package/dist/ui/assets/{audit-CqszEkOd.js → audit-CcAEDbZh.js} +1 -1
- package/dist/ui/assets/{chronos-CPwFWid9.js → chronos-2Z9E96_1.js} +1 -1
- package/dist/ui/assets/{config-D0DePxKu.js → config-DdfK4DX6.js} +1 -1
- package/dist/ui/assets/index-D4fzIKy1.css +1 -0
- package/dist/ui/assets/{index-BxVeRyTh.js → index-Dpd1Mkgp.js} +5 -5
- package/dist/ui/assets/{mcp-Gjc3IZpO.js → mcp-BWMt8aY7.js} +1 -1
- package/dist/ui/assets/{skills-B5DnmnHW.js → skills-D7JjK7JH.js} +1 -1
- package/dist/ui/assets/{stats-BAse7jj0.js → stats-DoIhtLot.js} +1 -1
- package/dist/ui/assets/{vendor-icons-BVuQI-6R.js → vendor-icons-DMd9RGvJ.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/AuditDashboard-nVV9KKFp.js +0 -1
- package/dist/ui/assets/MCPManager-BDjWMRRX.js +0 -1
- package/dist/ui/assets/SatiMemories-BxicQE35.js +0 -1
- package/dist/ui/assets/SessionAudit-CKJQf9LU.js +0 -9
- package/dist/ui/assets/Settings-CulMd4Qr.js +0 -41
- package/dist/ui/assets/Skills-DPoqYa8Y.js +0 -7
- package/dist/ui/assets/Smiths-Clamjlph.js +0 -1
- package/dist/ui/assets/Tasks-BfTkhB1J.js +0 -1
- package/dist/ui/assets/index-OLhpm8I7.css +0 -1
package/README.md
CHANGED
|
@@ -266,6 +266,13 @@ Telegram responses use rich HTML formatting conversion with:
|
|
|
266
266
|
|
|
267
267
|
Task results are delivered proactively with metadata (task id, agent, status) and output/error body.
|
|
268
268
|
|
|
269
|
+
**Session commands:**
|
|
270
|
+
- `/session` — Show current session info
|
|
271
|
+
- `/session list` — List recent sessions
|
|
272
|
+
- `/session new` — Create new session
|
|
273
|
+
- `/session switch <id>` — Switch to existing session
|
|
274
|
+
- `/session rename <name>` — Rename current session
|
|
275
|
+
|
|
269
276
|
**Voice messages:** Telegram voice messages are automatically transcribed (Gemini / Whisper / OpenRouter) and processed as text through the Oracle.
|
|
270
277
|
|
|
271
278
|
## Discord Experience
|
|
@@ -279,7 +286,11 @@ Discord bot responds to **DMs only** from authorized user IDs (`allowedUsers`).
|
|
|
279
286
|
| `/help` | Show available commands |
|
|
280
287
|
| `/status` | Check Morpheus status |
|
|
281
288
|
| `/stats` | Token usage statistics |
|
|
282
|
-
| `/
|
|
289
|
+
| `/session` | Show current session info |
|
|
290
|
+
| `/session_list` | List recent sessions |
|
|
291
|
+
| `/session_new` | Start a new session |
|
|
292
|
+
| `/session_switch id:` | Switch to existing session |
|
|
293
|
+
| `/session_rename name:` | Rename current session |
|
|
283
294
|
| `/mcps` | List MCP servers with tool counts |
|
|
284
295
|
| `/mcpreload` | Reload MCP connections and tools |
|
|
285
296
|
| `/mcp_enable name:` | Enable an MCP server |
|
|
@@ -314,16 +325,19 @@ Adding a new channel requires only implementing `IChannelAdapter` (`channel`, `s
|
|
|
314
325
|
## Web UI
|
|
315
326
|
|
|
316
327
|
The dashboard includes:
|
|
317
|
-
- Chat with session management
|
|
318
|
-
- Tasks page (stats, filters, details, retry)
|
|
328
|
+
- Chat with session management and browser notifications
|
|
329
|
+
- Tasks page (stats, filters, details, retry, pagination)
|
|
319
330
|
- Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
|
|
320
331
|
- MCP manager (add/edit/delete/toggle/reload)
|
|
321
|
-
- Sati memories (search, bulk delete)
|
|
332
|
+
- Sati memories (search, bulk delete, pagination)
|
|
322
333
|
- Usage stats and model pricing
|
|
323
334
|
- Trinity databases (register/test/refresh schema)
|
|
324
335
|
- Chronos scheduler (create/edit/delete jobs, execution history)
|
|
336
|
+
- Smiths management (add/edit/delete, real-time status, ping)
|
|
337
|
+
- Audit dashboard (session audit, tool call tracking, cost breakdowns)
|
|
325
338
|
- Webhooks and notification inbox
|
|
326
339
|
- Logs viewer
|
|
340
|
+
- Danger Zone (Settings → reset sessions, tasks, jobs, audit, or factory reset)
|
|
327
341
|
|
|
328
342
|
Chat-specific rendering:
|
|
329
343
|
- AI messages rendered as markdown
|
package/dist/config/manager.js
CHANGED
|
@@ -328,6 +328,10 @@ export class ConfigManager {
|
|
|
328
328
|
entries: config.smiths?.entries ?? [],
|
|
329
329
|
},
|
|
330
330
|
verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
|
|
331
|
+
setup: {
|
|
332
|
+
enabled: resolveBoolean('MORPHEUS_SETUP_ENABLED', config.setup?.enabled, true),
|
|
333
|
+
fields: config.setup?.fields ?? ['name', 'timezone', 'preferred_language'],
|
|
334
|
+
},
|
|
331
335
|
};
|
|
332
336
|
}
|
|
333
337
|
get() {
|
|
@@ -411,6 +415,13 @@ export class ConfigManager {
|
|
|
411
415
|
}
|
|
412
416
|
return defaults;
|
|
413
417
|
}
|
|
418
|
+
getSetupConfig() {
|
|
419
|
+
const defaults = { enabled: true, fields: ['name', 'timezone', 'preferred_language'] };
|
|
420
|
+
if (this.config.setup) {
|
|
421
|
+
return { ...defaults, ...this.config.setup };
|
|
422
|
+
}
|
|
423
|
+
return defaults;
|
|
424
|
+
}
|
|
414
425
|
getSmithsConfig() {
|
|
415
426
|
const defaults = {
|
|
416
427
|
enabled: false,
|
package/dist/config/schemas.js
CHANGED
|
@@ -63,6 +63,10 @@ export const SmithEntrySchema = z.object({
|
|
|
63
63
|
auth_token: z.string().min(1),
|
|
64
64
|
tls: z.boolean().default(false),
|
|
65
65
|
});
|
|
66
|
+
export const SetupConfigSchema = z.object({
|
|
67
|
+
enabled: z.boolean().default(true),
|
|
68
|
+
fields: z.array(z.string()).default(['name', 'timezone', 'preferred_language']),
|
|
69
|
+
});
|
|
66
70
|
export const SmithsConfigSchema = z.object({
|
|
67
71
|
enabled: z.boolean().default(false),
|
|
68
72
|
execution_mode: z.enum(['sync', 'async']).default('async'),
|
|
@@ -96,6 +100,7 @@ export const ConfigSchema = z.object({
|
|
|
96
100
|
chronos: ChronosConfigSchema.optional(),
|
|
97
101
|
devkit: DevKitConfigSchema.optional(),
|
|
98
102
|
smiths: SmithsConfigSchema.optional(),
|
|
103
|
+
setup: SetupConfigSchema.optional(),
|
|
99
104
|
verbose_mode: z.boolean().default(true),
|
|
100
105
|
channels: z.object({
|
|
101
106
|
telegram: z.object({
|
package/dist/http/api.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ChronosWorker } from '../runtime/chronos/worker.js';
|
|
|
20
20
|
import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
|
|
21
21
|
import { createSkillsRouter } from './routers/skills.js';
|
|
22
22
|
import { createSmithsRouter } from './routers/smiths.js';
|
|
23
|
+
import { createDangerRouter } from './routers/danger.js';
|
|
23
24
|
import { getActiveEnvOverrides } from '../config/precedence.js';
|
|
24
25
|
import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
|
|
25
26
|
import { AuditRepository } from '../runtime/audit/repository.js';
|
|
@@ -49,6 +50,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
49
50
|
router.use('/skills', createSkillsRouter());
|
|
50
51
|
// Mount Smiths router
|
|
51
52
|
router.use('/smiths', createSmithsRouter());
|
|
53
|
+
// Mount Danger Zone router
|
|
54
|
+
router.use('/danger', createDangerRouter());
|
|
52
55
|
// --- Session Management ---
|
|
53
56
|
router.get('/sessions', async (req, res) => {
|
|
54
57
|
try {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { SatiRepository } from '../../runtime/memory/sati/repository.js';
|
|
8
|
+
import { DisplayManager } from '../../runtime/display.js';
|
|
9
|
+
/**
|
|
10
|
+
* Valid categories the user can choose to delete.
|
|
11
|
+
*/
|
|
12
|
+
const VALID_CATEGORIES = [
|
|
13
|
+
'sessions', // sessions + messages
|
|
14
|
+
'memories', // sati-memory.db (long-term memory, embeddings, session chunks)
|
|
15
|
+
'tasks', // background tasks
|
|
16
|
+
'audit', // audit_events
|
|
17
|
+
'chronos', // chronos_jobs + chronos_executions
|
|
18
|
+
'webhooks', // webhooks + webhook_notifications
|
|
19
|
+
];
|
|
20
|
+
const ResetBodySchema = z.object({
|
|
21
|
+
categories: z.array(z.enum(VALID_CATEGORIES)).min(1, 'At least one category must be selected'),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Creates the Danger Zone API router.
|
|
25
|
+
* Provides destructive operations for resetting user data.
|
|
26
|
+
*/
|
|
27
|
+
export function createDangerRouter() {
|
|
28
|
+
const router = Router();
|
|
29
|
+
const display = DisplayManager.getInstance();
|
|
30
|
+
/**
|
|
31
|
+
* GET /api/danger/categories — List available reset categories
|
|
32
|
+
*/
|
|
33
|
+
router.get('/categories', (_req, res) => {
|
|
34
|
+
res.json({
|
|
35
|
+
categories: VALID_CATEGORIES.map((id) => ({ id })),
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* DELETE /api/danger/reset — Purge selected user data
|
|
40
|
+
*
|
|
41
|
+
* Body: { categories: ['sessions', 'memories', 'tasks', 'audit', 'chronos', 'webhooks'] }
|
|
42
|
+
*/
|
|
43
|
+
router.delete('/reset', async (req, res) => {
|
|
44
|
+
// Validate body
|
|
45
|
+
const parsed = ResetBodySchema.safeParse(req.body);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
return res.status(400).json({
|
|
48
|
+
error: 'Invalid request',
|
|
49
|
+
details: parsed.error.issues.map((i) => i.message),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const { categories } = parsed.data;
|
|
53
|
+
try {
|
|
54
|
+
const memoryDir = path.join(homedir(), '.morpheus', 'memory');
|
|
55
|
+
const shortMemoryPath = path.join(memoryDir, 'short-memory.db');
|
|
56
|
+
const satiMemoryPath = path.join(memoryDir, 'sati-memory.db');
|
|
57
|
+
const counts = {};
|
|
58
|
+
// ─── 1. Purge short-memory.db tables based on selected categories ───
|
|
59
|
+
const needsShortDb = categories.some((c) => ['sessions', 'tasks', 'audit', 'chronos', 'webhooks'].includes(c));
|
|
60
|
+
if (needsShortDb && fs.existsSync(shortMemoryPath)) {
|
|
61
|
+
const db = new Database(shortMemoryPath, { timeout: 5000 });
|
|
62
|
+
db.pragma('journal_mode = WAL');
|
|
63
|
+
const transaction = db.transaction(() => {
|
|
64
|
+
if (categories.includes('sessions')) {
|
|
65
|
+
const msgResult = db.prepare('DELETE FROM messages').run();
|
|
66
|
+
counts.messages = msgResult.changes;
|
|
67
|
+
const sessResult = db.prepare('DELETE FROM sessions').run();
|
|
68
|
+
counts.sessions = sessResult.changes;
|
|
69
|
+
// Also clear first-time setup state so onboarding runs again after reset
|
|
70
|
+
try {
|
|
71
|
+
db.prepare('DELETE FROM setup_state').run();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Table may not exist on older installations — safe to ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (categories.includes('tasks')) {
|
|
78
|
+
const taskResult = db.prepare('DELETE FROM tasks').run();
|
|
79
|
+
counts.tasks = taskResult.changes;
|
|
80
|
+
}
|
|
81
|
+
if (categories.includes('audit')) {
|
|
82
|
+
const auditResult = db.prepare('DELETE FROM audit_events').run();
|
|
83
|
+
counts.audit_events = auditResult.changes;
|
|
84
|
+
}
|
|
85
|
+
if (categories.includes('chronos')) {
|
|
86
|
+
const jobsResult = db.prepare('DELETE FROM chronos_jobs').run();
|
|
87
|
+
counts.chronos_jobs = jobsResult.changes;
|
|
88
|
+
const execResult = db.prepare('DELETE FROM chronos_executions').run();
|
|
89
|
+
counts.chronos_executions = execResult.changes;
|
|
90
|
+
}
|
|
91
|
+
if (categories.includes('webhooks')) {
|
|
92
|
+
const notifResult = db.prepare('DELETE FROM webhook_notifications').run();
|
|
93
|
+
counts.webhook_notifications = notifResult.changes;
|
|
94
|
+
const whResult = db.prepare('DELETE FROM webhooks').run();
|
|
95
|
+
counts.webhooks = whResult.changes;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
transaction();
|
|
99
|
+
db.close();
|
|
100
|
+
}
|
|
101
|
+
// ─── 2. Purge sati-memory.db (close, delete, recreate) ───
|
|
102
|
+
if (categories.includes('memories')) {
|
|
103
|
+
const satiRepo = SatiRepository.getInstance();
|
|
104
|
+
satiRepo.close();
|
|
105
|
+
if (fs.existsSync(satiMemoryPath)) {
|
|
106
|
+
fs.removeSync(satiMemoryPath);
|
|
107
|
+
fs.removeSync(satiMemoryPath + '-wal');
|
|
108
|
+
fs.removeSync(satiMemoryPath + '-shm');
|
|
109
|
+
counts.sati_memory = 1;
|
|
110
|
+
}
|
|
111
|
+
// Reinitialize so schema is recreated cleanly
|
|
112
|
+
satiRepo.initialize();
|
|
113
|
+
}
|
|
114
|
+
display.log(`🗑️ Data reset via Danger Zone: [${categories.join(', ')}]`, {
|
|
115
|
+
source: 'DangerZone',
|
|
116
|
+
level: 'warning',
|
|
117
|
+
});
|
|
118
|
+
res.json({
|
|
119
|
+
success: true,
|
|
120
|
+
message: 'Selected data has been reset successfully.',
|
|
121
|
+
categories,
|
|
122
|
+
deleted: counts,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
display.log(`❌ Danger Zone reset failed: ${error}`, {
|
|
127
|
+
source: 'DangerZone',
|
|
128
|
+
level: 'error',
|
|
129
|
+
});
|
|
130
|
+
res.status(500).json({
|
|
131
|
+
error: 'Failed to reset data',
|
|
132
|
+
details: error instanceof Error ? error.message : String(error),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return router;
|
|
137
|
+
}
|
|
@@ -190,6 +190,7 @@ export class AuditRepository {
|
|
|
190
190
|
SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
|
|
191
191
|
SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
|
|
192
192
|
SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
|
|
193
|
+
SUM(CASE WHEN ae.event_type = 'memory_persist' THEN 1 ELSE 0 END) as memoryPersistCount,
|
|
193
194
|
SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,
|
|
194
195
|
SUM(CASE WHEN ae.event_type = 'task_created' THEN 1 ELSE 0 END) as taskCreatedCount,
|
|
195
196
|
SUM(CASE WHEN ae.event_type = 'task_completed' THEN 1 ELSE 0 END) as taskCompletedCount,
|
|
@@ -304,6 +305,7 @@ export class AuditRepository {
|
|
|
304
305
|
mcpToolCount: totalsRow?.mcpToolCount ?? 0,
|
|
305
306
|
skillCount: totalsRow?.skillCount ?? 0,
|
|
306
307
|
memoryRecoveryCount: totalsRow?.memoryRecoveryCount ?? 0,
|
|
308
|
+
memoryPersistCount: totalsRow?.memoryPersistCount ?? 0,
|
|
307
309
|
chronosJobCount: totalsRow?.chronosJobCount ?? 0,
|
|
308
310
|
taskCreatedCount: totalsRow?.taskCreatedCount ?? 0,
|
|
309
311
|
taskCompletedCount: totalsRow?.taskCompletedCount ?? 0,
|
|
@@ -65,7 +65,7 @@ export class SatiMemoryMiddleware {
|
|
|
65
65
|
async afterAgent(generatedResponse, history, userSessionId) {
|
|
66
66
|
try {
|
|
67
67
|
await this.service.evaluateAndPersist([
|
|
68
|
-
...history.
|
|
68
|
+
...history.map(m => ({
|
|
69
69
|
role: m._getType() === 'human' ? 'user' : 'assistant',
|
|
70
70
|
content: m.content.toString()
|
|
71
71
|
})),
|
|
@@ -209,6 +209,33 @@ export class SatiService {
|
|
|
209
209
|
display.log(`Deletion skipped — memory not found: ${deletion.id}`, { source: 'Sati', level: 'warning' });
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
// Emit audit event for memory persistence results
|
|
213
|
+
const inclusionsCount = (result.inclusions ?? []).filter(i => i.summary && i.category && i.importance).length;
|
|
214
|
+
const editsCount = (result.edits ?? []).filter(e => !!e.id).length;
|
|
215
|
+
const deletionsCount = (result.deletions ?? []).filter(d => !!d.id).length;
|
|
216
|
+
const totalOps = inclusionsCount + editsCount + deletionsCount;
|
|
217
|
+
if (totalOps > 0) {
|
|
218
|
+
try {
|
|
219
|
+
AuditRepository.getInstance().insert({
|
|
220
|
+
session_id: userSessionId ?? 'sati-persist',
|
|
221
|
+
event_type: 'memory_persist',
|
|
222
|
+
agent: 'sati',
|
|
223
|
+
duration_ms: Date.now() - satiStartMs,
|
|
224
|
+
status: 'success',
|
|
225
|
+
metadata: {
|
|
226
|
+
inclusions_count: inclusionsCount,
|
|
227
|
+
edits_count: editsCount,
|
|
228
|
+
deletions_count: deletionsCount,
|
|
229
|
+
inclusions: (result.inclusions ?? []).filter(i => i.summary && i.category && i.importance).map(i => ({ category: i.category, importance: i.importance, summary: i.summary })),
|
|
230
|
+
edits: (result.edits ?? []).filter(e => !!e.id).map(e => ({ id: e.id, summary: e.summary, reason: e.reason })),
|
|
231
|
+
deletions: (result.deletions ?? []).filter(d => !!d.id).map(d => ({ id: d.id, reason: d.reason })),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
console.warn('[SatiService] Failed to log memory persistence audit event');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
212
239
|
}
|
|
213
240
|
catch (error) {
|
|
214
241
|
console.error('[SatiService] Evaluation failed:', error);
|
|
@@ -18,77 +18,84 @@ export async function runSessionEmbeddingWorker() {
|
|
|
18
18
|
// 🔥 importante: carregar vec0 no DB onde existe a tabela vetorial
|
|
19
19
|
loadVecExtension(satiDb);
|
|
20
20
|
const embeddingService = await EmbeddingService.getInstance();
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
const sessions = shortDb.prepare(`
|
|
23
24
|
SELECT id
|
|
24
25
|
FROM sessions
|
|
25
26
|
WHERE ended_at IS NOT NULL
|
|
26
27
|
AND embedding_status = 'pending'
|
|
27
28
|
LIMIT ?
|
|
28
29
|
`).all(BATCH_LIMIT);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
if (sessions.length === 0) {
|
|
31
|
+
// display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
for (const session of sessions) {
|
|
35
|
+
const sessionId = session.id;
|
|
36
|
+
display.log(`🧠 Processando sessão ${sessionId}...`, { source: 'SessionEmbeddingWorker' });
|
|
37
|
+
try {
|
|
38
|
+
// Skip setting 'processing' as it violates CHECK constraint
|
|
39
|
+
// active_processing.add(sessionId); // If we needed concurrency control
|
|
40
|
+
const chunks = satiDb.prepare(`
|
|
40
41
|
SELECT id, content
|
|
41
42
|
FROM session_chunks
|
|
42
43
|
WHERE session_id = ?
|
|
43
44
|
ORDER BY chunk_index
|
|
44
45
|
`).all(sessionId);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (chunks.length === 0) {
|
|
47
|
+
display.log(`⚠️ Sessão ${sessionId} não possui chunks.`, { source: 'SessionEmbeddingWorker' });
|
|
48
|
+
shortDb.prepare(`
|
|
48
49
|
UPDATE sessions
|
|
49
50
|
SET embedding_status = 'embedded',
|
|
50
51
|
embedded = 1
|
|
51
52
|
WHERE id = ?
|
|
52
53
|
`).run(sessionId);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const insertVec = satiDb.prepare(`
|
|
56
57
|
INSERT INTO session_vec (embedding)
|
|
57
58
|
VALUES (?)
|
|
58
59
|
`);
|
|
59
|
-
|
|
60
|
+
const insertMap = satiDb.prepare(`
|
|
60
61
|
INSERT OR REPLACE INTO session_embedding_map
|
|
61
62
|
(session_chunk_id, vec_rowid)
|
|
62
63
|
VALUES (?, ?)
|
|
63
64
|
`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
display.log(` ↳ Embedding chunk ${chunk.id}`, { source: 'SessionEmbeddingWorker' });
|
|
67
|
+
const embedding = await embeddingService.generate(chunk.content);
|
|
68
|
+
if (!embedding || embedding.length !== EMBEDDING_DIM) {
|
|
69
|
+
throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
|
|
70
|
+
}
|
|
71
|
+
const result = insertVec.run(new Float32Array(embedding));
|
|
72
|
+
const vecRowId = result.lastInsertRowid;
|
|
73
|
+
insertMap.run(chunk.id, vecRowId);
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
insertMap.run(chunk.id, vecRowId);
|
|
73
|
-
}
|
|
74
|
-
// ✅ finalizar sessão
|
|
75
|
-
shortDb.prepare(`
|
|
75
|
+
// ✅ finalizar sessão
|
|
76
|
+
shortDb.prepare(`
|
|
76
77
|
UPDATE sessions
|
|
77
78
|
SET embedding_status = 'embedded',
|
|
78
79
|
embedded = 1
|
|
79
80
|
WHERE id = ?
|
|
80
81
|
`).run(sessionId);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
display.log(`✅ Sessão ${sessionId} embedada com sucesso.`, { source: 'SessionEmbeddingWorker' });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
display.log(`❌ Erro na sessão ${sessionId}: ${err}`, { source: 'SessionEmbeddingWorker' });
|
|
86
|
+
shortDb.prepare(`
|
|
86
87
|
UPDATE sessions
|
|
87
88
|
SET embedding_status = 'failed'
|
|
88
89
|
WHERE id = ?
|
|
89
90
|
`).run(sessionId);
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
}
|
|
95
|
+
finally {
|
|
96
|
+
// Always close connections when done
|
|
97
|
+
shortDb.close();
|
|
98
|
+
satiDb.close();
|
|
99
|
+
}
|
|
93
100
|
// display.log('🏁 Worker finalizado.', { source: 'SessionEmbeddingWorker' });
|
|
94
101
|
}
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -17,6 +17,8 @@ import { MCPManager } from "../config/mcp-manager.js";
|
|
|
17
17
|
import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
|
|
18
18
|
import { SmithRegistry } from "./smiths/registry.js";
|
|
19
19
|
import { AuditRepository } from "./audit/repository.js";
|
|
20
|
+
import { SetupRepository } from './setup/repository.js';
|
|
21
|
+
import { buildSetupTool } from './tools/setup-tool.js';
|
|
20
22
|
import { emitToolAuditEvents } from "./subagent-utils.js";
|
|
21
23
|
const ORACLE_DELEGATION_TOOLS = new Set([
|
|
22
24
|
'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate',
|
|
@@ -154,7 +156,10 @@ export class Oracle {
|
|
|
154
156
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
155
157
|
updateSkillToolDescriptions();
|
|
156
158
|
// Build tool list — conditionally include SmithDelegateTool based on config
|
|
159
|
+
// Initialize setup repository (creates table if needed)
|
|
160
|
+
SetupRepository.getInstance();
|
|
157
161
|
const coreTools = [
|
|
162
|
+
buildSetupTool(),
|
|
158
163
|
TaskQueryTool,
|
|
159
164
|
Neo.getInstance().createDelegateTool(),
|
|
160
165
|
Apoc.getInstance().createDelegateTool(),
|
|
@@ -222,8 +227,25 @@ export class Oracle {
|
|
|
222
227
|
if (extraUsage) {
|
|
223
228
|
userMessage.usage_metadata = extraUsage;
|
|
224
229
|
}
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
// Build first-time setup block if setup is not yet completed
|
|
231
|
+
const setupRepo = SetupRepository.getInstance();
|
|
232
|
+
let setupBlock = '';
|
|
233
|
+
if (!setupRepo.isCompleted()) {
|
|
234
|
+
const missingFields = setupRepo.getMissingFields();
|
|
235
|
+
if (missingFields.length > 0) {
|
|
236
|
+
setupBlock = `## [FIRST-TIME SETUP — ACTIVE]
|
|
237
|
+
Before responding to any other request, you MUST collect the user's basic information.
|
|
238
|
+
Ask for the following fields conversationally (one or two at a time — do NOT list them all at once):
|
|
239
|
+
${missingFields.map((f) => `- ${f}`).join('\n')}
|
|
240
|
+
|
|
241
|
+
Once the user provides a value, immediately call \`setup_save\` with the collected fields.
|
|
242
|
+
Do NOT proceed with other tasks until all required fields have been collected and saved.
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const systemMessage = new SystemMessage(`${setupBlock}You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
|
|
227
249
|
|
|
228
250
|
You are an orchestrator and task router.
|
|
229
251
|
|
|
@@ -623,6 +645,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
623
645
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
624
646
|
updateSkillToolDescriptions();
|
|
625
647
|
this.provider = await ProviderFactory.create(this.config.llm, [
|
|
648
|
+
buildSetupTool(),
|
|
626
649
|
TaskQueryTool,
|
|
627
650
|
Neo.getInstance().createDelegateTool(),
|
|
628
651
|
Apoc.getInstance().createDelegateTool(),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs-extra';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { SetupRepository } from '../repository.js';
|
|
6
|
+
import { ConfigManager } from '../../../config/manager.js';
|
|
7
|
+
// Mock ConfigManager so tests are not coupled to the real config file
|
|
8
|
+
vi.mock('../../../config/manager.js', () => ({
|
|
9
|
+
ConfigManager: {
|
|
10
|
+
getInstance: vi.fn(() => ({
|
|
11
|
+
getSetupConfig: vi.fn(() => ({ enabled: true, fields: ['name', 'city'] })),
|
|
12
|
+
})),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
describe('SetupRepository', () => {
|
|
16
|
+
let testDbPath;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const tempDir = path.join(tmpdir(), 'morpheus-setup-test', Date.now().toString());
|
|
19
|
+
fs.ensureDirSync(tempDir);
|
|
20
|
+
testDbPath = path.join(tempDir, 'short-memory.db');
|
|
21
|
+
// Reset singleton so each test gets a fresh instance
|
|
22
|
+
SetupRepository.resetInstance();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
SetupRepository.resetInstance();
|
|
26
|
+
// Clean up temp DB
|
|
27
|
+
const tempDir = path.dirname(testDbPath);
|
|
28
|
+
try {
|
|
29
|
+
fs.removeSync(tempDir);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore — Windows may delay file release
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
describe('initialize()', () => {
|
|
36
|
+
it('should create setup_state table on construction', () => {
|
|
37
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
38
|
+
// If table was created, isCompleted() should not throw
|
|
39
|
+
expect(() => repo.isCompleted()).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('isCompleted()', () => {
|
|
43
|
+
it('returns false when __completed__ record does not exist', () => {
|
|
44
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
45
|
+
expect(repo.isCompleted()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('returns true after markCompleted() is called', () => {
|
|
48
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
49
|
+
repo.markCompleted();
|
|
50
|
+
expect(repo.isCompleted()).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('returns true when setup.enabled is false even without __completed__ record', () => {
|
|
53
|
+
// Override mock for this test only
|
|
54
|
+
vi.mocked(ConfigManager.getInstance).mockReturnValueOnce({
|
|
55
|
+
getSetupConfig: vi.fn(() => ({ enabled: false, fields: ['name'] })),
|
|
56
|
+
});
|
|
57
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
58
|
+
expect(repo.isCompleted()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('saveField() / getMissingFields()', () => {
|
|
62
|
+
it('getMissingFields returns all configured fields initially', () => {
|
|
63
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
64
|
+
const missing = repo.getMissingFields();
|
|
65
|
+
expect(missing).toEqual(['name', 'city']);
|
|
66
|
+
});
|
|
67
|
+
it('removes a field from missing list after saving it', () => {
|
|
68
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
69
|
+
repo.saveField('name', 'João');
|
|
70
|
+
const missing = repo.getMissingFields();
|
|
71
|
+
expect(missing).toEqual(['city']);
|
|
72
|
+
expect(missing).not.toContain('name');
|
|
73
|
+
});
|
|
74
|
+
it('returns empty array when all fields are saved', () => {
|
|
75
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
76
|
+
repo.saveField('name', 'João');
|
|
77
|
+
repo.saveField('city', 'Brasília');
|
|
78
|
+
expect(repo.getMissingFields()).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
it('upserts a field on second save', () => {
|
|
81
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
82
|
+
repo.saveField('name', 'João');
|
|
83
|
+
repo.saveField('name', 'Carlos');
|
|
84
|
+
// Should still have only one entry for name (no duplicate)
|
|
85
|
+
expect(repo.getMissingFields()).toEqual(['city']);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('markCompleted()', () => {
|
|
89
|
+
it('is idempotent (can be called multiple times without error)', () => {
|
|
90
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
91
|
+
expect(() => {
|
|
92
|
+
repo.markCompleted();
|
|
93
|
+
repo.markCompleted();
|
|
94
|
+
repo.markCompleted();
|
|
95
|
+
}).not.toThrow();
|
|
96
|
+
expect(repo.isCompleted()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('reset()', () => {
|
|
100
|
+
it('clears all records including __completed__', () => {
|
|
101
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
102
|
+
repo.saveField('name', 'João');
|
|
103
|
+
repo.saveField('city', 'Brasília');
|
|
104
|
+
repo.markCompleted();
|
|
105
|
+
expect(repo.isCompleted()).toBe(true);
|
|
106
|
+
repo.reset();
|
|
107
|
+
expect(repo.isCompleted()).toBe(false);
|
|
108
|
+
expect(repo.getMissingFields()).toEqual(['name', 'city']);
|
|
109
|
+
});
|
|
110
|
+
it('is safe to call on empty table', () => {
|
|
111
|
+
const repo = SetupRepository.getInstance(testDbPath);
|
|
112
|
+
expect(() => repo.reset()).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|