morpheus-cli 0.5.5 → 0.6.0
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 +44 -2
- package/dist/channels/telegram.js +264 -7
- package/dist/cli/commands/start.js +9 -1
- package/dist/config/manager.js +18 -1
- package/dist/config/schemas.js +6 -0
- package/dist/http/api.js +24 -2
- package/dist/http/routers/chronos.js +267 -0
- package/dist/http/server.js +4 -2
- package/dist/runtime/chronos/parser.js +215 -0
- package/dist/runtime/chronos/parser.test.js +63 -0
- package/dist/runtime/chronos/repository.js +244 -0
- package/dist/runtime/chronos/worker.js +141 -0
- package/dist/runtime/chronos/worker.test.js +120 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/memory/sqlite.js +10 -0
- package/dist/runtime/oracle.js +17 -4
- package/dist/runtime/tasks/dispatcher.js +8 -4
- package/dist/runtime/tasks/repository.js +17 -4
- package/dist/runtime/tools/chronos-tools.js +181 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/ui/assets/index-BDWWF6gM.css +1 -0
- package/dist/ui/assets/index-DVQvTlPe.js +111 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +6 -1
- package/dist/ui/assets/index-DP2V4kRd.js +0 -112
- package/dist/ui/assets/index-mglRG5Zw.css +0 -1
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@ 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).
|
|
12
|
+
- Multi-agent architecture (Oracle, Neo, Apoc, Sati, Trinity).
|
|
13
13
|
- Async task execution with queue + worker + notifier.
|
|
14
|
+
- Chronos temporal scheduler for recurring and one-time Oracle executions.
|
|
14
15
|
- Rich operational visibility in UI (chat traces, tasks, usage, logs).
|
|
15
16
|
|
|
16
17
|
## Multi-Agent Roles
|
|
@@ -19,6 +20,7 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
|
|
|
19
20
|
- `Apoc`: DevTools/browser execution (filesystem, shell, git, network, packages, processes, system, browser automation).
|
|
20
21
|
- `Sati`: long-term memory retrieval/evaluation.
|
|
21
22
|
- `Trinity`: database specialist. Executes queries, introspects schemas, and manages registered databases (PostgreSQL, MySQL, SQLite, MongoDB).
|
|
23
|
+
- `Chronos`: temporal scheduler. Runs Oracle prompts on a recurring or one-time schedule.
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -156,6 +158,36 @@ Important behavior:
|
|
|
156
158
|
- Duplicate/fabricated task acknowledgements are blocked by validation against DB.
|
|
157
159
|
- Status follow-ups are handled by Oracle through `task_query` (no delegation required).
|
|
158
160
|
|
|
161
|
+
## Chronos — Temporal Scheduler
|
|
162
|
+
|
|
163
|
+
Chronos lets you schedule any Oracle prompt to run at a fixed time or on a recurring schedule.
|
|
164
|
+
|
|
165
|
+
**Schedule types:**
|
|
166
|
+
- `once` — run once at a specific time: `"in 30 minutes"`, `"tomorrow at 9am"`, `"2026-03-01T09:00:00"`
|
|
167
|
+
- `interval` — recurring natural language: `"every day at 9am"`, `"every weekday"`, `"every monday and friday at 8am"`, `"every 30 minutes"`
|
|
168
|
+
- `cron` — raw 5-field cron: `"0 9 * * 1-5"`
|
|
169
|
+
|
|
170
|
+
**Execution model:**
|
|
171
|
+
- Jobs run inside the currently active Oracle session — no isolated sessions are created per job.
|
|
172
|
+
- Chronos injects context as an AI message before invoking `oracle.chat()`, keeping conversation history clean.
|
|
173
|
+
- Delegated tasks spawned during Chronos execution carry `origin_channel: 'telegram'` when a Telegram notify function is registered, so task results are delivered to you.
|
|
174
|
+
|
|
175
|
+
**Oracle tools:** `chronos_schedule`, `chronos_list`, `chronos_cancel`, `chronos_preview`
|
|
176
|
+
|
|
177
|
+
**Telegram commands:**
|
|
178
|
+
- `/chronos <prompt> @ <schedule>` — create a job
|
|
179
|
+
- `/chronos_list` — list all jobs (🟢 active / 🔴 disabled)
|
|
180
|
+
- `/chronos_view <id>` — view job details and last 5 executions
|
|
181
|
+
- `/chronos_enable <id>` / `/chronos_disable <id>` / `/chronos_delete <id>`
|
|
182
|
+
|
|
183
|
+
**API endpoints (protected):**
|
|
184
|
+
- `GET/POST /api/chronos` — list / create jobs
|
|
185
|
+
- `GET/PUT/DELETE /api/chronos/:id` — read / update / delete
|
|
186
|
+
- `PATCH /api/chronos/:id/enable` / `.../disable`
|
|
187
|
+
- `GET /api/chronos/:id/executions`
|
|
188
|
+
- `POST /api/chronos/preview` — preview next N run timestamps
|
|
189
|
+
- `GET/POST/DELETE /api/config/chronos`
|
|
190
|
+
|
|
159
191
|
## Telegram Experience
|
|
160
192
|
|
|
161
193
|
Telegram responses use rich HTML formatting conversion with:
|
|
@@ -175,6 +207,7 @@ The dashboard includes:
|
|
|
175
207
|
- Sati memories (search, bulk delete)
|
|
176
208
|
- Usage stats and model pricing
|
|
177
209
|
- Trinity databases (register/test/refresh schema)
|
|
210
|
+
- Chronos scheduler (create/edit/delete jobs, execution history)
|
|
178
211
|
- Webhooks and notification inbox
|
|
179
212
|
- Logs viewer
|
|
180
213
|
|
|
@@ -223,6 +256,10 @@ trinity:
|
|
|
223
256
|
model: gpt-4o-mini
|
|
224
257
|
temperature: 0.2
|
|
225
258
|
|
|
259
|
+
chronos:
|
|
260
|
+
check_interval_ms: 60000 # polling interval in ms (minimum 60000)
|
|
261
|
+
default_timezone: UTC # IANA timezone used when none is specified
|
|
262
|
+
|
|
226
263
|
runtime:
|
|
227
264
|
async_tasks:
|
|
228
265
|
enabled: true
|
|
@@ -352,10 +389,11 @@ Authenticated endpoints (`x-architect-pass`):
|
|
|
352
389
|
- Sessions: `/api/sessions*`
|
|
353
390
|
- Chat: `POST /api/chat`
|
|
354
391
|
- Tasks: `GET /api/tasks`, `GET /api/tasks/stats`, `GET /api/tasks/:id`, `POST /api/tasks/:id/retry`
|
|
355
|
-
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`
|
|
392
|
+
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`, `/api/config/chronos`
|
|
356
393
|
- MCP: `/api/mcp/*` (servers CRUD + reload + status)
|
|
357
394
|
- Sati memories: `/api/sati/memories*`
|
|
358
395
|
- Trinity databases: `GET/POST/PUT/DELETE /api/trinity/databases`, `POST /api/trinity/databases/:id/test`, `POST /api/trinity/databases/:id/refresh-schema`
|
|
396
|
+
- Chronos: `GET/POST /api/chronos`, `GET/PUT/DELETE /api/chronos/:id`, `PATCH /api/chronos/:id/enable`, `PATCH /api/chronos/:id/disable`, `GET /api/chronos/:id/executions`, `POST /api/chronos/preview`
|
|
359
397
|
- Usage/model pricing/logs/restart
|
|
360
398
|
- Webhook management and webhook notifications
|
|
361
399
|
|
|
@@ -462,6 +500,10 @@ src/
|
|
|
462
500
|
trinity.ts
|
|
463
501
|
trinity-connector.ts # PostgreSQL/MySQL/SQLite/MongoDB drivers
|
|
464
502
|
trinity-crypto.ts # AES-256-GCM encryption for DB passwords
|
|
503
|
+
chronos/
|
|
504
|
+
worker.ts # polling timer and job execution
|
|
505
|
+
repository.ts # SQLite-backed job and execution store
|
|
506
|
+
parser.ts # natural-language schedule parser
|
|
465
507
|
memory/
|
|
466
508
|
tasks/
|
|
467
509
|
tools/
|
|
@@ -123,6 +123,14 @@ function escMd(value) {
|
|
|
123
123
|
// The - must be at end of character class to avoid being treated as a range.
|
|
124
124
|
return String(value).replace(/([.!?(){}#+~|=>$@\\-])/g, '\\$1');
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Full MarkdownV2 escape — escapes ALL special characters including * _ ` [ ].
|
|
128
|
+
* Use for untrusted/user-generated content (session titles, prompts, etc.)
|
|
129
|
+
* placed inside bold/italic markers or anywhere in a MarkdownV2 message.
|
|
130
|
+
*/
|
|
131
|
+
function escMdRaw(value) {
|
|
132
|
+
return String(value).replace(/([_*[\]()~`>#+=|{}.!\\-])/g, '\\$1');
|
|
133
|
+
}
|
|
126
134
|
export class TelegramAdapter {
|
|
127
135
|
bot = null;
|
|
128
136
|
isConnected = false;
|
|
@@ -135,6 +143,8 @@ export class TelegramAdapter {
|
|
|
135
143
|
history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
136
144
|
RATE_LIMIT_MS = 3000; // minimum ms between requests per user
|
|
137
145
|
rateLimiter = new Map(); // userId -> last request timestamp
|
|
146
|
+
// Pending Chronos create confirmations (userId -> job data + expiry)
|
|
147
|
+
pendingChronosCreate = new Map();
|
|
138
148
|
isRateLimited(userId) {
|
|
139
149
|
const now = Date.now();
|
|
140
150
|
const last = this.rateLimiter.get(userId);
|
|
@@ -155,7 +165,13 @@ export class TelegramAdapter {
|
|
|
155
165
|
/sessions \\- List all sessions with titles and switch between them
|
|
156
166
|
/restart \\- Restart the Morpheus agent
|
|
157
167
|
/mcpreload \\- Reload MCP servers without restarting
|
|
158
|
-
/mcp or /mcps \\- List registered MCP servers
|
|
168
|
+
/mcp or /mcps \\- List registered MCP servers
|
|
169
|
+
/chronos <prompt \\+ time\\> \\- Schedule a prompt for the Oracle
|
|
170
|
+
/chronos\\_list \\- List all active Chronos jobs
|
|
171
|
+
/chronos\\_view <id\\> \\- View a Chronos job and its last executions
|
|
172
|
+
/chronos\\_disable <id\\> \\- Disable a Chronos job
|
|
173
|
+
/chronos\\_enable <id\\> \\- Enable a Chronos job
|
|
174
|
+
/chronos\\_delete <id\\> \\- Delete a Chronos job`;
|
|
159
175
|
constructor(oracle) {
|
|
160
176
|
this.oracle = oracle;
|
|
161
177
|
}
|
|
@@ -187,6 +203,22 @@ export class TelegramAdapter {
|
|
|
187
203
|
await this.handleSystemCommand(ctx, text, user);
|
|
188
204
|
return;
|
|
189
205
|
}
|
|
206
|
+
// Check for pending Chronos create confirmation
|
|
207
|
+
const pendingChronos = this.pendingChronosCreate.get(userId);
|
|
208
|
+
if (pendingChronos) {
|
|
209
|
+
const lower = text.trim().toLowerCase();
|
|
210
|
+
if (lower === 'yes' || lower === 'confirm' || lower === 'y') {
|
|
211
|
+
clearTimeout(pendingChronos.timer);
|
|
212
|
+
this.pendingChronosCreate.delete(userId);
|
|
213
|
+
await this.confirmChronosCreate(ctx, userId, pendingChronos);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
clearTimeout(pendingChronos.timer);
|
|
217
|
+
this.pendingChronosCreate.delete(userId);
|
|
218
|
+
await ctx.reply('Cancelled.');
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
190
222
|
// Rate limit check
|
|
191
223
|
if (this.isRateLimited(userId)) {
|
|
192
224
|
await ctx.reply('Please wait a moment before sending another message.');
|
|
@@ -738,10 +770,234 @@ export class TelegramAdapter {
|
|
|
738
770
|
case '/sessions':
|
|
739
771
|
await this.handleSessionStatusCommand(ctx, user);
|
|
740
772
|
break;
|
|
773
|
+
case '/chronos': {
|
|
774
|
+
const userId = ctx.from?.id?.toString() ?? user;
|
|
775
|
+
const fullText = text.slice('/chronos'.length).trim();
|
|
776
|
+
await this.handleChronosCreate(ctx, userId, fullText);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
case '/chronos_list':
|
|
780
|
+
await this.handleChronosList(ctx);
|
|
781
|
+
break;
|
|
782
|
+
case '/chronos_view': {
|
|
783
|
+
const id = args[0] ?? '';
|
|
784
|
+
await this.handleChronosView(ctx, id);
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case '/chronos_disable': {
|
|
788
|
+
const id = args[0] ?? '';
|
|
789
|
+
await this.handleChronosDisable(ctx, id);
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
case '/chronos_enable': {
|
|
793
|
+
const id = args[0] ?? '';
|
|
794
|
+
await this.handleChronosEnable(ctx, id);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
case '/chronos_delete': {
|
|
798
|
+
const id = args[0] ?? '';
|
|
799
|
+
await this.handleChronosDelete(ctx, id);
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
741
802
|
default:
|
|
742
803
|
await this.handleDefaultCommand(ctx, user, command);
|
|
743
804
|
}
|
|
744
805
|
}
|
|
806
|
+
// ─── Chronos Command Handlers ────────────────────────────────────────────────
|
|
807
|
+
async handleChronosCreate(ctx, userId, fullText) {
|
|
808
|
+
if (!fullText) {
|
|
809
|
+
await ctx.reply('Usage: /chronos <prompt + time expression>\nExample: /chronos Check disk space tomorrow at 9am');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
const { parse: chronoParse } = await import('chrono-node');
|
|
814
|
+
const results = chronoParse(fullText);
|
|
815
|
+
if (!results.length) {
|
|
816
|
+
await ctx.reply('Could not detect a time expression. Try: `/chronos Check disk space tomorrow at 9am`', { parse_mode: 'Markdown' });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// Extract the first matched datetime fragment and derive the prompt
|
|
820
|
+
const match = results[0];
|
|
821
|
+
const matchedText = fullText.slice(match.index, match.index + match.text.length);
|
|
822
|
+
const prompt = (fullText.slice(0, match.index) + fullText.slice(match.index + match.text.length)).replace(/\s+/g, ' ').trim() || fullText;
|
|
823
|
+
const globalTz = this.config.getChronosConfig().timezone;
|
|
824
|
+
const { parseScheduleExpression } = await import('../runtime/chronos/parser.js');
|
|
825
|
+
const schedule = parseScheduleExpression(matchedText, 'once', { timezone: globalTz });
|
|
826
|
+
const formatted = new Date(schedule.next_run_at).toLocaleString('en-US', {
|
|
827
|
+
timeZone: globalTz, year: 'numeric', month: 'short', day: 'numeric',
|
|
828
|
+
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
|
|
829
|
+
});
|
|
830
|
+
const timer = setTimeout(() => {
|
|
831
|
+
this.pendingChronosCreate.delete(userId);
|
|
832
|
+
}, 5 * 60 * 1000);
|
|
833
|
+
this.pendingChronosCreate.set(userId, {
|
|
834
|
+
prompt,
|
|
835
|
+
schedule_expression: matchedText,
|
|
836
|
+
human_readable: schedule.human_readable,
|
|
837
|
+
timezone: globalTz,
|
|
838
|
+
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
839
|
+
timer,
|
|
840
|
+
});
|
|
841
|
+
await ctx.reply(`📅 *${prompt}*\n${schedule.human_readable} (${formatted})\n\nConfirm? Reply \`yes\` or \`no\``, { parse_mode: 'Markdown' });
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async confirmChronosCreate(ctx, _userId, pending) {
|
|
848
|
+
if (Date.now() > pending.expiresAt) {
|
|
849
|
+
await ctx.reply('Confirmation expired. Please run /chronos again.');
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const { parseScheduleExpression } = await import('../runtime/chronos/parser.js');
|
|
854
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
855
|
+
const schedule = parseScheduleExpression(pending.schedule_expression, 'once', {
|
|
856
|
+
timezone: pending.timezone,
|
|
857
|
+
});
|
|
858
|
+
const repo = ChronosRepository.getInstance();
|
|
859
|
+
const job = repo.createJob({
|
|
860
|
+
prompt: pending.prompt,
|
|
861
|
+
schedule_type: 'once',
|
|
862
|
+
schedule_expression: pending.schedule_expression,
|
|
863
|
+
cron_normalized: schedule.cron_normalized,
|
|
864
|
+
timezone: pending.timezone,
|
|
865
|
+
next_run_at: schedule.next_run_at,
|
|
866
|
+
created_by: 'telegram',
|
|
867
|
+
});
|
|
868
|
+
const formatted = new Date(schedule.next_run_at).toLocaleString('en-US', {
|
|
869
|
+
timeZone: pending.timezone, year: 'numeric', month: 'short', day: 'numeric',
|
|
870
|
+
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
|
|
871
|
+
});
|
|
872
|
+
await ctx.reply(`✅ Job created (ID: \`${job.id.slice(0, 8)}\`)\n${schedule.human_readable}\n${formatted}`, {
|
|
873
|
+
parse_mode: 'Markdown',
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
await ctx.reply(`Failed to create job: ${err.message}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async handleChronosList(ctx) {
|
|
881
|
+
try {
|
|
882
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
883
|
+
const repo = ChronosRepository.getInstance();
|
|
884
|
+
const jobs = repo.listJobs();
|
|
885
|
+
if (!jobs.length) {
|
|
886
|
+
await ctx.reply('No Chronos jobs found.');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const lines = jobs.map((j, i) => {
|
|
890
|
+
const status = j.enabled ? '🟢' : '🔴';
|
|
891
|
+
const next = j.enabled && j.next_run_at
|
|
892
|
+
? new Date(j.next_run_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
893
|
+
: j.enabled ? 'N/A' : 'disabled';
|
|
894
|
+
const prompt = j.prompt.length > 35 ? j.prompt.slice(0, 35) + '…' : j.prompt;
|
|
895
|
+
return `${status} ${i + 1}. \`${j.id}\` \n${prompt}\n _${next}_`;
|
|
896
|
+
});
|
|
897
|
+
await ctx.reply(`*Chronos Jobs*\n\n${lines.join('\n')}`, { parse_mode: 'Markdown' });
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async handleChronosView(ctx, id) {
|
|
904
|
+
if (!id) {
|
|
905
|
+
await ctx.reply('Usage: /chronos_view <job_id>');
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
910
|
+
const repo = ChronosRepository.getInstance();
|
|
911
|
+
const job = repo.getJob(id);
|
|
912
|
+
if (!job) {
|
|
913
|
+
await ctx.reply('Job not found.');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const executions = repo.listExecutions(id, 3);
|
|
917
|
+
const next = job.next_run_at ? new Date(job.next_run_at).toLocaleString() : 'N/A';
|
|
918
|
+
const last = job.last_run_at ? new Date(job.last_run_at).toLocaleString() : 'Never';
|
|
919
|
+
const execLines = executions.map(e => ` • ${e.status.toUpperCase()} — ${new Date(e.triggered_at).toLocaleString()}`).join('\n') || ' None yet';
|
|
920
|
+
const msg = `*Chronos Job* \`${id.slice(0, 8)}\`\n\n` +
|
|
921
|
+
`*Prompt:* ${job.prompt}\n` +
|
|
922
|
+
`*Schedule:* ${job.schedule_type} — \`${job.schedule_expression}\`\n` +
|
|
923
|
+
`*Timezone:* ${job.timezone}\n` +
|
|
924
|
+
`*Status:* ${job.enabled ? 'Enabled' : 'Disabled'}\n` +
|
|
925
|
+
`*Next Run:* ${next}\n` +
|
|
926
|
+
`*Last Run:* ${last}\n\n` +
|
|
927
|
+
`*Last 3 Executions:*\n${execLines}`;
|
|
928
|
+
await ctx.reply(msg, { parse_mode: 'Markdown' });
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async handleChronosDisable(ctx, id) {
|
|
935
|
+
if (!id) {
|
|
936
|
+
await ctx.reply('Usage: /chronos_disable <job_id>');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
941
|
+
const repo = ChronosRepository.getInstance();
|
|
942
|
+
const job = repo.disableJob(id);
|
|
943
|
+
if (!job) {
|
|
944
|
+
await ctx.reply('Job not found.');
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
await ctx.reply(`Job \`${id.slice(0, 8)}\` disabled.`, { parse_mode: 'Markdown' });
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async handleChronosEnable(ctx, id) {
|
|
954
|
+
if (!id) {
|
|
955
|
+
await ctx.reply('Usage: /chronos_enable <job_id>');
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
960
|
+
const { parseNextRun } = await import('../runtime/chronos/parser.js');
|
|
961
|
+
const repo = ChronosRepository.getInstance();
|
|
962
|
+
const existing = repo.getJob(id);
|
|
963
|
+
if (!existing) {
|
|
964
|
+
await ctx.reply('Job not found.');
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
let nextRunAt;
|
|
968
|
+
if (existing.cron_normalized) {
|
|
969
|
+
// cron_normalized is always a 5-field cron string — use parseNextRun directly
|
|
970
|
+
nextRunAt = parseNextRun(existing.cron_normalized, existing.timezone);
|
|
971
|
+
}
|
|
972
|
+
repo.updateJob(id, { enabled: true, next_run_at: nextRunAt });
|
|
973
|
+
const job = repo.getJob(id);
|
|
974
|
+
const next = job.next_run_at ? new Date(job.next_run_at).toLocaleString() : 'N/A';
|
|
975
|
+
await ctx.reply(`Job \`${id.slice(0, 8)}\` enabled. Next run: ${next}`, { parse_mode: 'Markdown' });
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async handleChronosDelete(ctx, id) {
|
|
982
|
+
if (!id) {
|
|
983
|
+
await ctx.reply('Usage: /chronos_delete <job_id>');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
const { ChronosRepository } = await import('../runtime/chronos/repository.js');
|
|
988
|
+
const repo = ChronosRepository.getInstance();
|
|
989
|
+
const deleted = repo.deleteJob(id);
|
|
990
|
+
if (!deleted) {
|
|
991
|
+
await ctx.reply('Job not found.');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
await ctx.reply(`Job \`${id.slice(0, 8)}\` deleted.`, { parse_mode: 'Markdown' });
|
|
995
|
+
}
|
|
996
|
+
catch (err) {
|
|
997
|
+
await ctx.reply(`Error: ${err.message}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// ─── End Chronos ──────────────────────────────────────────────────────────────
|
|
745
1001
|
async handleNewSessionCommand(ctx, user) {
|
|
746
1002
|
try {
|
|
747
1003
|
await ctx.reply("Are you ready to start a new session\\? Please confirm\\.", {
|
|
@@ -767,9 +1023,10 @@ export class TelegramAdapter {
|
|
|
767
1023
|
}
|
|
768
1024
|
async handleSessionStatusCommand(ctx, user) {
|
|
769
1025
|
try {
|
|
770
|
-
// Obter todas as sessões ativas e pausadas usando a nova função
|
|
771
1026
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
772
|
-
|
|
1027
|
+
// Exclude automated Chronos sessions — their IDs exceed Telegram's 64-byte
|
|
1028
|
+
// callback_data limit and they are not user-managed sessions.
|
|
1029
|
+
const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
|
|
773
1030
|
if (sessions.length === 0) {
|
|
774
1031
|
await ctx.reply('No active or paused sessions found\\.', { parse_mode: 'MarkdownV2' });
|
|
775
1032
|
return;
|
|
@@ -779,10 +1036,10 @@ export class TelegramAdapter {
|
|
|
779
1036
|
for (const session of sessions) {
|
|
780
1037
|
const title = session.title || 'Untitled Session';
|
|
781
1038
|
const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
|
|
782
|
-
response += `${statusEmoji} *${
|
|
783
|
-
response += `\\- ID:
|
|
784
|
-
response += `\\- Status: ${
|
|
785
|
-
response += `\\- Started: ${
|
|
1039
|
+
response += `${statusEmoji} *${escMdRaw(title)}*\n`;
|
|
1040
|
+
response += `\\- ID: \`${escMdRaw(session.id)}\`\n`;
|
|
1041
|
+
response += `\\- Status: ${escMdRaw(session.status)}\n`;
|
|
1042
|
+
response += `\\- Started: ${escMdRaw(new Date(session.started_at).toLocaleString())}\n\n`;
|
|
786
1043
|
// Adicionar botão inline para alternar para esta sessão
|
|
787
1044
|
const sessionButtons = [];
|
|
788
1045
|
if (session.status !== 'active') {
|
|
@@ -18,6 +18,8 @@ import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-
|
|
|
18
18
|
import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
19
19
|
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
20
20
|
import { TaskDispatcher } from '../../runtime/tasks/dispatcher.js';
|
|
21
|
+
import { ChronosWorker } from '../../runtime/chronos/worker.js';
|
|
22
|
+
import { ChronosRepository } from '../../runtime/chronos/repository.js';
|
|
21
23
|
export const startCommand = new Command('start')
|
|
22
24
|
.description('Start the Morpheus agent')
|
|
23
25
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -125,10 +127,13 @@ export const startCommand = new Command('start')
|
|
|
125
127
|
const taskWorker = new TaskWorker();
|
|
126
128
|
const taskNotifier = new TaskNotifier();
|
|
127
129
|
const asyncTasksEnabled = config.runtime?.async_tasks?.enabled !== false;
|
|
130
|
+
const chronosRepo = ChronosRepository.getInstance();
|
|
131
|
+
const chronosWorker = new ChronosWorker(chronosRepo, oracle);
|
|
132
|
+
ChronosWorker.setInstance(chronosWorker);
|
|
128
133
|
// Initialize Web UI
|
|
129
134
|
if (options.ui && config.ui.enabled) {
|
|
130
135
|
try {
|
|
131
|
-
httpServer = new HttpServer(oracle);
|
|
136
|
+
httpServer = new HttpServer(oracle, chronosWorker);
|
|
132
137
|
// Use CLI port if provided and valid, otherwise fallback to config or default
|
|
133
138
|
const port = parseInt(options.port) || config.ui.port || 3333;
|
|
134
139
|
httpServer.start(port);
|
|
@@ -146,6 +151,7 @@ export const startCommand = new Command('start')
|
|
|
146
151
|
// Wire Telegram adapter to webhook dispatcher for proactive notifications
|
|
147
152
|
WebhookDispatcher.setTelegramAdapter(telegram);
|
|
148
153
|
TaskDispatcher.setTelegramAdapter(telegram);
|
|
154
|
+
ChronosWorker.setNotifyFn((text) => telegram.sendMessage(text));
|
|
149
155
|
adapters.push(telegram);
|
|
150
156
|
}
|
|
151
157
|
catch (e) {
|
|
@@ -158,6 +164,7 @@ export const startCommand = new Command('start')
|
|
|
158
164
|
}
|
|
159
165
|
// Start Background Services
|
|
160
166
|
startSessionEmbeddingScheduler();
|
|
167
|
+
chronosWorker.start();
|
|
161
168
|
if (asyncTasksEnabled) {
|
|
162
169
|
taskWorker.start();
|
|
163
170
|
taskNotifier.start();
|
|
@@ -176,6 +183,7 @@ export const startCommand = new Command('start')
|
|
|
176
183
|
for (const adapter of adapters) {
|
|
177
184
|
await adapter.disconnect();
|
|
178
185
|
}
|
|
186
|
+
chronosWorker.stop();
|
|
179
187
|
if (asyncTasksEnabled) {
|
|
180
188
|
taskWorker.stop();
|
|
181
189
|
taskNotifier.stop();
|
package/dist/config/manager.js
CHANGED
|
@@ -186,6 +186,15 @@ export class ConfigManager {
|
|
|
186
186
|
const memoryConfig = {
|
|
187
187
|
limit: config.memory.limit // Not applying env var precedence to deprecated field
|
|
188
188
|
};
|
|
189
|
+
// Apply precedence to Chronos config
|
|
190
|
+
let chronosConfig;
|
|
191
|
+
if (config.chronos) {
|
|
192
|
+
chronosConfig = {
|
|
193
|
+
timezone: resolveString('MORPHEUS_CHRONOS_TIMEZONE', config.chronos.timezone, 'UTC'),
|
|
194
|
+
check_interval_ms: resolveNumeric('MORPHEUS_CHRONOS_CHECK_INTERVAL_MS', config.chronos.check_interval_ms, 60000),
|
|
195
|
+
max_active_jobs: resolveNumeric('MORPHEUS_CHRONOS_MAX_ACTIVE_JOBS', config.chronos.max_active_jobs, 100),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
189
198
|
return {
|
|
190
199
|
agent: agentConfig,
|
|
191
200
|
llm: llmConfig,
|
|
@@ -197,7 +206,8 @@ export class ConfigManager {
|
|
|
197
206
|
channels: channelsConfig,
|
|
198
207
|
ui: uiConfig,
|
|
199
208
|
logging: loggingConfig,
|
|
200
|
-
memory: memoryConfig
|
|
209
|
+
memory: memoryConfig,
|
|
210
|
+
chronos: chronosConfig,
|
|
201
211
|
};
|
|
202
212
|
}
|
|
203
213
|
get() {
|
|
@@ -267,4 +277,11 @@ export class ConfigManager {
|
|
|
267
277
|
// Fallback to main LLM config
|
|
268
278
|
return { ...this.config.llm };
|
|
269
279
|
}
|
|
280
|
+
getChronosConfig() {
|
|
281
|
+
const defaults = { timezone: 'UTC', check_interval_ms: 60000, max_active_jobs: 100 };
|
|
282
|
+
if (this.config.chronos) {
|
|
283
|
+
return { ...defaults, ...this.config.chronos };
|
|
284
|
+
}
|
|
285
|
+
return defaults;
|
|
286
|
+
}
|
|
270
287
|
}
|
package/dist/config/schemas.js
CHANGED
|
@@ -31,6 +31,11 @@ export const TrinityConfigSchema = LLMConfigSchema;
|
|
|
31
31
|
export const WebhookConfigSchema = z.object({
|
|
32
32
|
telegram_notify_all: z.boolean().optional(),
|
|
33
33
|
}).optional();
|
|
34
|
+
export const ChronosConfigSchema = z.object({
|
|
35
|
+
timezone: z.string().default('UTC'),
|
|
36
|
+
check_interval_ms: z.number().min(60000).default(60000),
|
|
37
|
+
max_active_jobs: z.number().min(1).max(1000).default(100),
|
|
38
|
+
});
|
|
34
39
|
// Zod Schema matching MorpheusConfig interface
|
|
35
40
|
export const ConfigSchema = z.object({
|
|
36
41
|
agent: z.object({
|
|
@@ -52,6 +57,7 @@ export const ConfigSchema = z.object({
|
|
|
52
57
|
enabled: z.boolean().default(DEFAULT_CONFIG.runtime?.async_tasks.enabled ?? true),
|
|
53
58
|
}).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
|
|
54
59
|
}).optional(),
|
|
60
|
+
chronos: ChronosConfigSchema.optional(),
|
|
55
61
|
channels: z.object({
|
|
56
62
|
telegram: z.object({
|
|
57
63
|
enabled: z.boolean().default(false),
|
package/dist/http/api.js
CHANGED
|
@@ -15,6 +15,9 @@ import { TaskRepository } from '../runtime/tasks/repository.js';
|
|
|
15
15
|
import { DatabaseRegistry } from '../runtime/memory/trinity-db.js';
|
|
16
16
|
import { testConnection, introspectSchema } from '../runtime/trinity-connector.js';
|
|
17
17
|
import { Trinity } from '../runtime/trinity.js';
|
|
18
|
+
import { ChronosRepository } from '../runtime/chronos/repository.js';
|
|
19
|
+
import { ChronosWorker } from '../runtime/chronos/worker.js';
|
|
20
|
+
import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
|
|
18
21
|
async function readLastLines(filePath, n) {
|
|
19
22
|
try {
|
|
20
23
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -25,11 +28,18 @@ async function readLastLines(filePath, n) {
|
|
|
25
28
|
return [];
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
export function createApiRouter(oracle) {
|
|
31
|
+
export function createApiRouter(oracle, chronosWorker) {
|
|
29
32
|
const router = Router();
|
|
30
33
|
const configManager = ConfigManager.getInstance();
|
|
31
34
|
const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
32
35
|
const taskRepository = TaskRepository.getInstance();
|
|
36
|
+
const chronosRepo = ChronosRepository.getInstance();
|
|
37
|
+
const worker = chronosWorker ?? ChronosWorker.getInstance();
|
|
38
|
+
// Mount Chronos routers
|
|
39
|
+
if (worker) {
|
|
40
|
+
router.use('/chronos', createChronosJobRouter(chronosRepo, worker));
|
|
41
|
+
router.use('/config/chronos', createChronosConfigRouter(worker));
|
|
42
|
+
}
|
|
33
43
|
// --- Session Management ---
|
|
34
44
|
router.get('/sessions', async (req, res) => {
|
|
35
45
|
try {
|
|
@@ -248,6 +258,18 @@ export function createApiRouter(oracle) {
|
|
|
248
258
|
res.status(500).json({ error: err.message });
|
|
249
259
|
}
|
|
250
260
|
});
|
|
261
|
+
router.post('/tasks/:id/cancel', (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const ok = taskRepository.cancelTask(req.params.id);
|
|
264
|
+
if (!ok) {
|
|
265
|
+
return res.status(404).json({ error: 'Active task not found for cancellation' });
|
|
266
|
+
}
|
|
267
|
+
res.json({ success: true });
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
res.status(500).json({ error: err.message });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
251
273
|
// Legacy /session/reset (keep for backward compat or redirect to POST /sessions)
|
|
252
274
|
router.post('/session/reset', async (req, res) => {
|
|
253
275
|
try {
|
|
@@ -999,7 +1021,7 @@ export function createApiRouter(oracle) {
|
|
|
999
1021
|
return res.status(404).json({ error: 'Log file not found' });
|
|
1000
1022
|
}
|
|
1001
1023
|
const lines = await readLastLines(filePath, limit);
|
|
1002
|
-
res.json({ lines: lines
|
|
1024
|
+
res.json({ lines: lines });
|
|
1003
1025
|
});
|
|
1004
1026
|
return router;
|
|
1005
1027
|
}
|