morpheus-cli 0.9.32 → 0.9.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/memory/sati/service.js +13 -3
- package/dist/runtime/memory/sqlite.js +66 -1
- package/dist/runtime/oracle.js +18 -7
- package/dist/runtime/tasks/event-bus.js +11 -0
- package/dist/runtime/tasks/notifier.js +57 -31
- package/dist/runtime/tasks/repository.js +21 -0
- package/dist/runtime/tasks/worker.js +11 -7
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@ const display = DisplayManager.getInstance();
|
|
|
12
12
|
export class SatiService {
|
|
13
13
|
repository;
|
|
14
14
|
static instance;
|
|
15
|
+
cachedAgent = null;
|
|
16
|
+
cachedAgentConfigKey = null;
|
|
15
17
|
constructor() {
|
|
16
18
|
this.repository = SatiRepository.getInstance();
|
|
17
19
|
}
|
|
@@ -59,9 +61,17 @@ export class SatiService {
|
|
|
59
61
|
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
60
62
|
if (!satiConfig)
|
|
61
63
|
return;
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
// Reuse cached agent when config hasn't changed to avoid per-call overhead
|
|
65
|
+
const configKey = `${satiConfig.provider}:${satiConfig.model}`;
|
|
66
|
+
let agent;
|
|
67
|
+
if (this.cachedAgent && this.cachedAgentConfigKey === configKey) {
|
|
68
|
+
agent = this.cachedAgent;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
agent = await ProviderFactory.create(satiConfig, []);
|
|
72
|
+
this.cachedAgent = agent;
|
|
73
|
+
this.cachedAgentConfigKey = configKey;
|
|
74
|
+
}
|
|
65
75
|
// Get existing memories for context (Simulated "Working Memory" or full list if small)
|
|
66
76
|
const allMemories = this.repository.getAllMemories();
|
|
67
77
|
// Map conversation to strict types and sanitize
|
|
@@ -10,6 +10,48 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
10
10
|
lc_namespace = ["langchain", "stores", "message", "sqlite"];
|
|
11
11
|
display = DisplayManager.getInstance();
|
|
12
12
|
static migrationDone = false; // run migrations only once per process
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// In-memory message cache — eliminates repeated SQLite reads for active sessions
|
|
15
|
+
// Key: sessionId Value: { messages (DESC order, newest first), touchedAt, limit }
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
static _cache = new Map();
|
|
18
|
+
static _CACHE_MAX_SESSIONS = 50;
|
|
19
|
+
static _CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
|
+
/** Populate or replace a cache entry, evicting LRU if over capacity. */
|
|
21
|
+
static _setCacheEntry(sessionId, messages, limit) {
|
|
22
|
+
if (!sessionId)
|
|
23
|
+
return;
|
|
24
|
+
if (SQLiteChatMessageHistory._cache.size >= SQLiteChatMessageHistory._CACHE_MAX_SESSIONS) {
|
|
25
|
+
let oldestKey = '';
|
|
26
|
+
let oldestTime = Infinity;
|
|
27
|
+
for (const [k, v] of SQLiteChatMessageHistory._cache) {
|
|
28
|
+
if (v.touchedAt < oldestTime) {
|
|
29
|
+
oldestTime = v.touchedAt;
|
|
30
|
+
oldestKey = k;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (oldestKey)
|
|
34
|
+
SQLiteChatMessageHistory._cache.delete(oldestKey);
|
|
35
|
+
}
|
|
36
|
+
SQLiteChatMessageHistory._cache.set(sessionId, { messages: [...messages], touchedAt: Date.now(), limit });
|
|
37
|
+
}
|
|
38
|
+
/** Remove a session from the cache (used on clear()). */
|
|
39
|
+
static invalidateCacheForSession(sessionId) {
|
|
40
|
+
SQLiteChatMessageHistory._cache.delete(sessionId);
|
|
41
|
+
}
|
|
42
|
+
/** Prepend new messages (newest-first order) to an existing cache entry. */
|
|
43
|
+
static _appendToCache(sessionId, newMessages) {
|
|
44
|
+
if (!sessionId || newMessages.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
const entry = SQLiteChatMessageHistory._cache.get(sessionId);
|
|
47
|
+
if (!entry)
|
|
48
|
+
return; // session not cached — will be populated on next read
|
|
49
|
+
// newMessages is in chronological order (oldest first); reverse so newest goes first
|
|
50
|
+
const newestFirst = [...newMessages].reverse();
|
|
51
|
+
const merged = [...newestFirst, ...entry.messages];
|
|
52
|
+
entry.messages = merged.slice(0, entry.limit);
|
|
53
|
+
entry.touchedAt = Date.now();
|
|
54
|
+
}
|
|
13
55
|
db;
|
|
14
56
|
sessionId;
|
|
15
57
|
limit;
|
|
@@ -281,6 +323,21 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
281
323
|
*/
|
|
282
324
|
async getMessages() {
|
|
283
325
|
try {
|
|
326
|
+
// -----------------------------------------------------------------------
|
|
327
|
+
// Cache fast-path: return cached messages if session is warm and not stale
|
|
328
|
+
// -----------------------------------------------------------------------
|
|
329
|
+
if (this.sessionId) {
|
|
330
|
+
const cached = SQLiteChatMessageHistory._cache.get(this.sessionId);
|
|
331
|
+
if (cached) {
|
|
332
|
+
const isStale = Date.now() - cached.touchedAt > SQLiteChatMessageHistory._CACHE_TTL_MS;
|
|
333
|
+
if (!isStale) {
|
|
334
|
+
cached.touchedAt = Date.now();
|
|
335
|
+
return [...cached.messages]; // defensive copy
|
|
336
|
+
}
|
|
337
|
+
// Stale — evict and fall through to DB
|
|
338
|
+
SQLiteChatMessageHistory._cache.delete(this.sessionId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
284
341
|
// Fetch new columns
|
|
285
342
|
const stmt = this.db.prepare(`SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model
|
|
286
343
|
FROM messages
|
|
@@ -358,7 +415,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
358
415
|
// Messages are in DESC order (newest first) here — orphans appear at the tail.
|
|
359
416
|
// Remove trailing ToolMessages and AIMessages with tool_calls that have no
|
|
360
417
|
// matching ToolMessage response (i.e. incomplete tool-call groups at the boundary).
|
|
361
|
-
|
|
418
|
+
const result = this.sanitizeMessageWindow(mapped);
|
|
419
|
+
// Populate cache for subsequent reads in this session
|
|
420
|
+
SQLiteChatMessageHistory._setCacheEntry(this.sessionId, result, this.limit ?? 100);
|
|
421
|
+
return result;
|
|
362
422
|
}
|
|
363
423
|
catch (error) {
|
|
364
424
|
// Check if it's a database lock error
|
|
@@ -459,6 +519,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
459
519
|
}
|
|
460
520
|
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, audio_duration_seconds, agent, duration_ms, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
461
521
|
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model, audioDurationSeconds, agent, durationMs, source);
|
|
522
|
+
// Update in-memory cache so the next getMessages() call is a cache hit
|
|
523
|
+
SQLiteChatMessageHistory._appendToCache(this.sessionId, [message]);
|
|
462
524
|
// Verificar se a sessão tem título e definir automaticamente se necessário
|
|
463
525
|
await this.setSessionTitleIfNeeded();
|
|
464
526
|
}
|
|
@@ -517,6 +579,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
517
579
|
});
|
|
518
580
|
try {
|
|
519
581
|
insertAll(messages);
|
|
582
|
+
// Update in-memory cache so the next getMessages() call is a cache hit
|
|
583
|
+
SQLiteChatMessageHistory._appendToCache(this.sessionId, messages);
|
|
520
584
|
await this.setSessionTitleIfNeeded();
|
|
521
585
|
}
|
|
522
586
|
catch (error) {
|
|
@@ -743,6 +807,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
743
807
|
try {
|
|
744
808
|
const stmt = this.db.prepare("DELETE FROM messages WHERE session_id = ?");
|
|
745
809
|
stmt.run(this.sessionId);
|
|
810
|
+
SQLiteChatMessageHistory._cache.delete(this.sessionId);
|
|
746
811
|
}
|
|
747
812
|
catch (error) {
|
|
748
813
|
// Check for database lock errors
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -19,7 +19,7 @@ import { SetupRepository } from './setup/repository.js';
|
|
|
19
19
|
import { buildSetupTool } from './tools/setup-tool.js';
|
|
20
20
|
import { SmithDelegator } from "./smiths/delegator.js";
|
|
21
21
|
import { PATHS } from "../config/paths.js";
|
|
22
|
-
import {
|
|
22
|
+
import { writeFile } from "fs/promises";
|
|
23
23
|
export class Oracle {
|
|
24
24
|
provider;
|
|
25
25
|
config;
|
|
@@ -200,6 +200,13 @@ export class Oracle {
|
|
|
200
200
|
this.registerSmithIfEnabled();
|
|
201
201
|
// Refresh dynamic tool catalogs so delegate descriptions contain runtime info.
|
|
202
202
|
await SubagentRegistry.refreshAllCatalogs();
|
|
203
|
+
// Eagerly initialize subagents so first delegation doesn't pay init cost.
|
|
204
|
+
await Promise.allSettled([
|
|
205
|
+
Apoc.getInstance().initialize(),
|
|
206
|
+
Neo.getInstance().initialize(),
|
|
207
|
+
Trinity.getInstance().initialize(),
|
|
208
|
+
Link.getInstance().initialize(),
|
|
209
|
+
]);
|
|
203
210
|
// Initialize setup repository (creates table if needed)
|
|
204
211
|
SetupRepository.getInstance();
|
|
205
212
|
const coreTools = [
|
|
@@ -459,10 +466,7 @@ ${SkillRegistry.getInstance().getSystemPromptSection()}
|
|
|
459
466
|
${SmithRegistry.getInstance().getSystemPromptSection()}
|
|
460
467
|
`);
|
|
461
468
|
//save the system prompt on ~/.morpheus/system_prompt.txt for debugging and prompt engineering purposes
|
|
462
|
-
|
|
463
|
-
writeFileSync(`${PATHS.root}/system_prompt.txt`, String(systemMessage.content), 'utf-8');
|
|
464
|
-
}
|
|
465
|
-
catch { }
|
|
469
|
+
writeFile(`${PATHS.root}/system_prompt.txt`, String(systemMessage.content), 'utf-8').catch(() => { });
|
|
466
470
|
// Resolve the authoritative session ID for this call.
|
|
467
471
|
// Priority: explicit taskContext > current history instance > fallback.
|
|
468
472
|
const currentSessionId = taskContext?.session_id
|
|
@@ -482,13 +486,20 @@ ${SmithRegistry.getInstance().getSystemPromptSection()}
|
|
|
482
486
|
// Load existing history from database in reverse order (most recent first)
|
|
483
487
|
let previousMessages = await callHistory.getMessages();
|
|
484
488
|
previousMessages = previousMessages.reverse();
|
|
485
|
-
// Sati Middleware: Retrieval
|
|
489
|
+
// Sati Middleware: Retrieval (with 3s timeout to avoid blocking the response)
|
|
486
490
|
let memoryMessage = null;
|
|
487
491
|
try {
|
|
488
|
-
|
|
492
|
+
const satiTimeout = new Promise((resolve) => setTimeout(() => resolve(null), 3000));
|
|
493
|
+
memoryMessage = await Promise.race([
|
|
494
|
+
this.satiMiddleware.beforeAgent(message, previousMessages, currentSessionId),
|
|
495
|
+
satiTimeout,
|
|
496
|
+
]);
|
|
489
497
|
if (memoryMessage) {
|
|
490
498
|
this.display.log('Sati memory retrieved.', { source: 'Sati' });
|
|
491
499
|
}
|
|
500
|
+
else if (memoryMessage === null) {
|
|
501
|
+
// Could be timeout or no memories found — either way, proceed
|
|
502
|
+
}
|
|
492
503
|
}
|
|
493
504
|
catch (e) {
|
|
494
505
|
// Fail open - do not disrupt main flow
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
/**
|
|
3
|
+
* Singleton event bus for task lifecycle events.
|
|
4
|
+
* Emitted by TaskWorker, consumed by TaskNotifier for immediate dispatch.
|
|
5
|
+
*
|
|
6
|
+
* Events:
|
|
7
|
+
* 'task:ready' (taskId: string) — task is completed/failed/cancelled and ready to notify
|
|
8
|
+
*/
|
|
9
|
+
class TaskEventBus extends EventEmitter {
|
|
10
|
+
}
|
|
11
|
+
export const taskEventBus = new TaskEventBus();
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { DisplayManager } from '../display.js';
|
|
2
2
|
import { TaskDispatcher } from './dispatcher.js';
|
|
3
3
|
import { TaskRepository } from './repository.js';
|
|
4
|
+
import { taskEventBus } from './event-bus.js';
|
|
4
5
|
export class TaskNotifier {
|
|
5
|
-
pollIntervalMs;
|
|
6
6
|
maxAttempts;
|
|
7
7
|
staleSendingMs;
|
|
8
|
-
|
|
8
|
+
recoveryPollMs;
|
|
9
9
|
repository = TaskRepository.getInstance();
|
|
10
10
|
display = DisplayManager.getInstance();
|
|
11
11
|
timer = null;
|
|
12
|
-
|
|
12
|
+
inFlight = new Set(); // task IDs currently being dispatched
|
|
13
13
|
constructor(opts) {
|
|
14
|
-
this.pollIntervalMs = opts?.pollIntervalMs ?? 1200;
|
|
15
14
|
this.maxAttempts = opts?.maxAttempts ?? 5;
|
|
16
15
|
this.staleSendingMs = opts?.staleSendingMs ?? 30_000;
|
|
17
|
-
|
|
16
|
+
// Slow poll only for orphan recovery (process restarts, crash scenarios)
|
|
17
|
+
this.recoveryPollMs = opts?.recoveryPollMs ?? 30_000;
|
|
18
18
|
}
|
|
19
19
|
start() {
|
|
20
20
|
if (this.timer)
|
|
@@ -26,43 +26,69 @@ export class TaskNotifier {
|
|
|
26
26
|
level: 'warning',
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
// Primary path: event-driven — fires immediately when TaskWorker completes a task
|
|
30
|
+
taskEventBus.on('task:ready', (taskId) => {
|
|
31
|
+
this.dispatchById(taskId);
|
|
32
|
+
});
|
|
33
|
+
// Fallback path: slow poll for orphaned tasks (e.g. completed before notifier started,
|
|
34
|
+
// or process restarted mid-notification)
|
|
29
35
|
this.timer = setInterval(() => {
|
|
30
|
-
void this.
|
|
31
|
-
}, this.
|
|
32
|
-
this.display.log('Task notifier started.', { source: 'TaskNotifier' });
|
|
36
|
+
void this.recoveryTick();
|
|
37
|
+
}, this.recoveryPollMs);
|
|
38
|
+
this.display.log('Task notifier started (event-driven + 30s recovery poll).', { source: 'TaskNotifier' });
|
|
33
39
|
}
|
|
34
40
|
stop() {
|
|
41
|
+
taskEventBus.removeAllListeners('task:ready');
|
|
35
42
|
if (this.timer) {
|
|
36
43
|
clearInterval(this.timer);
|
|
37
44
|
this.timer = null;
|
|
38
|
-
this.display.log('Task notifier stopped.', { source: 'TaskNotifier' });
|
|
39
45
|
}
|
|
46
|
+
this.display.log('Task notifier stopped.', { source: 'TaskNotifier' });
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Event-driven dispatch: called immediately when a task is marked complete/failed.
|
|
50
|
+
* Uses claimNotificationById to atomically claim — prevents double-dispatch with recovery poll.
|
|
51
|
+
*/
|
|
52
|
+
dispatchById(taskId) {
|
|
53
|
+
if (this.inFlight.has(taskId))
|
|
43
54
|
return;
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
this.inFlight.add(taskId);
|
|
56
|
+
const task = this.repository.claimNotificationById(taskId);
|
|
57
|
+
if (!task) {
|
|
58
|
+
// Already claimed by recovery poll or another path
|
|
59
|
+
this.inFlight.delete(taskId);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
void this.dispatch(task).finally(() => this.inFlight.delete(taskId));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fallback recovery: picks up any orphaned completed tasks not yet notified.
|
|
66
|
+
*/
|
|
67
|
+
async recoveryTick() {
|
|
68
|
+
// Drain all pending orphans in one recovery sweep
|
|
69
|
+
while (true) {
|
|
70
|
+
const task = this.repository.claimNextNotificationCandidate(0);
|
|
47
71
|
if (!task)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
const latest = this.repository.getTaskById(task.id);
|
|
55
|
-
const attempts = (latest?.notify_attempts ?? 0) + 1;
|
|
56
|
-
const retry = attempts < this.maxAttempts;
|
|
57
|
-
this.repository.markNotificationFailed(task.id, err?.message ?? String(err), retry);
|
|
58
|
-
this.display.log(`Task notification failed (${task.id}): ${err?.message ?? err}`, {
|
|
59
|
-
source: 'TaskNotifier',
|
|
60
|
-
level: retry ? 'warning' : 'error',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
72
|
+
break;
|
|
73
|
+
if (this.inFlight.has(task.id))
|
|
74
|
+
break; // already being dispatched via event
|
|
75
|
+
await this.dispatch(task);
|
|
63
76
|
}
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
}
|
|
78
|
+
async dispatch(task) {
|
|
79
|
+
try {
|
|
80
|
+
await TaskDispatcher.onTaskFinished(task);
|
|
81
|
+
this.repository.markNotificationSent(task.id);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const latest = this.repository.getTaskById(task.id);
|
|
85
|
+
const attempts = (latest?.notify_attempts ?? 0) + 1;
|
|
86
|
+
const retry = attempts < this.maxAttempts;
|
|
87
|
+
this.repository.markNotificationFailed(task.id, err?.message ?? String(err), retry);
|
|
88
|
+
this.display.log(`Task notification failed (${task.id}): ${err?.message ?? err}`, {
|
|
89
|
+
source: 'TaskNotifier',
|
|
90
|
+
level: retry ? 'warning' : 'error',
|
|
91
|
+
});
|
|
66
92
|
}
|
|
67
93
|
}
|
|
68
94
|
}
|
|
@@ -393,6 +393,27 @@ export class TaskRepository {
|
|
|
393
393
|
`).run(now, now, now - staleMs);
|
|
394
394
|
return result.changes;
|
|
395
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Atomically claim a specific task for notification by ID.
|
|
398
|
+
* Returns the task record if successfully claimed, null if already claimed or not eligible.
|
|
399
|
+
*/
|
|
400
|
+
claimNotificationById(taskId) {
|
|
401
|
+
const tx = this.db.transaction(() => {
|
|
402
|
+
const changed = this.db.prepare(`
|
|
403
|
+
UPDATE tasks
|
|
404
|
+
SET notify_status = 'sending',
|
|
405
|
+
notify_last_error = NULL,
|
|
406
|
+
updated_at = ?
|
|
407
|
+
WHERE id = ?
|
|
408
|
+
AND status IN ('completed', 'failed', 'cancelled')
|
|
409
|
+
AND notify_status = 'pending'
|
|
410
|
+
`).run(Date.now(), taskId);
|
|
411
|
+
if (changed.changes === 0)
|
|
412
|
+
return null;
|
|
413
|
+
return this.getTaskById(taskId);
|
|
414
|
+
});
|
|
415
|
+
return tx();
|
|
416
|
+
}
|
|
396
417
|
claimNextNotificationCandidate(minFinishedAgeMs = 0) {
|
|
397
418
|
const now = Date.now();
|
|
398
419
|
const tx = this.db.transaction(() => {
|
|
@@ -3,6 +3,7 @@ import { DisplayManager } from '../display.js';
|
|
|
3
3
|
import { SubagentRegistry } from '../subagents/registry.js';
|
|
4
4
|
import { TaskRepository } from './repository.js';
|
|
5
5
|
import { AuditRepository } from '../audit/repository.js';
|
|
6
|
+
import { taskEventBus } from './event-bus.js';
|
|
6
7
|
export class TaskWorker {
|
|
7
8
|
workerId;
|
|
8
9
|
pollIntervalMs;
|
|
@@ -38,13 +39,14 @@ export class TaskWorker {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
tick() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// Claim as many tasks as concurrency allows per tick
|
|
43
|
+
while (this.activeTasks.size < this.maxConcurrent) {
|
|
44
|
+
const task = this.repository.claimNextPending(this.workerId);
|
|
45
|
+
if (!task)
|
|
46
|
+
break;
|
|
47
|
+
this.activeTasks.add(task.id);
|
|
48
|
+
this.executeTask(task).finally(() => this.activeTasks.delete(task.id));
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
51
|
async executeTask(task) {
|
|
50
52
|
const audit = AuditRepository.getInstance();
|
|
@@ -94,6 +96,7 @@ export class TaskWorker {
|
|
|
94
96
|
});
|
|
95
97
|
}
|
|
96
98
|
this.display.log(`Task completed: ${task.id}`, { source: 'TaskWorker', level: 'success' });
|
|
99
|
+
taskEventBus.emit('task:ready', task.id);
|
|
97
100
|
}
|
|
98
101
|
catch (err) {
|
|
99
102
|
const latest = this.repository.getTaskById(task.id);
|
|
@@ -116,6 +119,7 @@ export class TaskWorker {
|
|
|
116
119
|
metadata: { error: errorMessage },
|
|
117
120
|
});
|
|
118
121
|
this.display.log(`Task failed: ${task.id} (${errorMessage})`, { source: 'TaskWorker', level: 'error' });
|
|
122
|
+
taskEventBus.emit('task:ready', task.id);
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.33",
|
|
4
4
|
"description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"morpheus": "./bin/morpheus.js"
|