morpheus-cli 0.5.6 → 0.6.1

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.
@@ -0,0 +1,244 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ import { ConfigManager } from '../../config/manager.js';
7
+ export class ChronosError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'ChronosError';
11
+ }
12
+ }
13
+ export class ChronosRepository {
14
+ static instance = null;
15
+ db;
16
+ constructor() {
17
+ const dbPath = path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
18
+ fs.ensureDirSync(path.dirname(dbPath));
19
+ this.db = new Database(dbPath, { timeout: 5000 });
20
+ this.db.pragma('journal_mode = WAL');
21
+ this.db.pragma('foreign_keys = ON');
22
+ this.ensureTable();
23
+ }
24
+ static getInstance() {
25
+ if (!ChronosRepository.instance) {
26
+ ChronosRepository.instance = new ChronosRepository();
27
+ }
28
+ return ChronosRepository.instance;
29
+ }
30
+ ensureTable() {
31
+ this.db.exec(`
32
+ CREATE TABLE IF NOT EXISTS chronos_jobs (
33
+ id TEXT PRIMARY KEY
34
+ );
35
+ `);
36
+ this.db.exec(`
37
+ CREATE TABLE IF NOT EXISTS chronos_executions (
38
+ id TEXT PRIMARY KEY
39
+ );
40
+ `);
41
+ this.migrateTable();
42
+ this.ensureIndexes();
43
+ }
44
+ migrateTable() {
45
+ const jobInfo = this.db.pragma('table_info(chronos_jobs)');
46
+ const jobCols = new Set(jobInfo.map((c) => c.name));
47
+ const addJobCol = (sql, col) => {
48
+ if (jobCols.has(col))
49
+ return;
50
+ this.db.exec(sql);
51
+ jobCols.add(col);
52
+ };
53
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN prompt TEXT NOT NULL DEFAULT ''`, 'prompt');
54
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN schedule_type TEXT NOT NULL DEFAULT 'once'`, 'schedule_type');
55
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN schedule_expression TEXT NOT NULL DEFAULT ''`, 'schedule_expression');
56
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN cron_normalized TEXT`, 'cron_normalized');
57
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN timezone TEXT NOT NULL DEFAULT 'UTC'`, 'timezone');
58
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN next_run_at INTEGER`, 'next_run_at');
59
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN last_run_at INTEGER`, 'last_run_at');
60
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1`, 'enabled');
61
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0`, 'created_at');
62
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0`, 'updated_at');
63
+ addJobCol(`ALTER TABLE chronos_jobs ADD COLUMN created_by TEXT NOT NULL DEFAULT 'api'`, 'created_by');
64
+ const execInfo = this.db.pragma('table_info(chronos_executions)');
65
+ const execCols = new Set(execInfo.map((c) => c.name));
66
+ const addExecCol = (sql, col) => {
67
+ if (execCols.has(col))
68
+ return;
69
+ this.db.exec(sql);
70
+ execCols.add(col);
71
+ };
72
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN job_id TEXT NOT NULL DEFAULT ''`, 'job_id');
73
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN triggered_at INTEGER NOT NULL DEFAULT 0`, 'triggered_at');
74
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN completed_at INTEGER`, 'completed_at');
75
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN status TEXT NOT NULL DEFAULT 'running'`, 'status');
76
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN error TEXT`, 'error');
77
+ addExecCol(`ALTER TABLE chronos_executions ADD COLUMN session_id TEXT NOT NULL DEFAULT ''`, 'session_id');
78
+ }
79
+ ensureIndexes() {
80
+ this.db.exec(`
81
+ CREATE INDEX IF NOT EXISTS idx_chronos_jobs_next_run
82
+ ON chronos_jobs (enabled, next_run_at);
83
+ CREATE INDEX IF NOT EXISTS idx_chronos_jobs_created_by
84
+ ON chronos_jobs (created_by);
85
+ CREATE INDEX IF NOT EXISTS idx_chronos_executions_job
86
+ ON chronos_executions (job_id, triggered_at DESC);
87
+ `);
88
+ }
89
+ deserializeJob(row) {
90
+ return {
91
+ id: row.id,
92
+ prompt: row.prompt,
93
+ schedule_type: row.schedule_type,
94
+ schedule_expression: row.schedule_expression,
95
+ cron_normalized: row.cron_normalized ?? null,
96
+ timezone: row.timezone,
97
+ next_run_at: row.next_run_at ?? null,
98
+ last_run_at: row.last_run_at ?? null,
99
+ enabled: row.enabled === 1,
100
+ created_at: row.created_at,
101
+ updated_at: row.updated_at,
102
+ created_by: row.created_by,
103
+ };
104
+ }
105
+ deserializeExecution(row) {
106
+ return {
107
+ id: row.id,
108
+ job_id: row.job_id,
109
+ triggered_at: row.triggered_at,
110
+ completed_at: row.completed_at ?? null,
111
+ status: row.status,
112
+ error: row.error ?? null,
113
+ session_id: row.session_id,
114
+ };
115
+ }
116
+ // ─── T036: max_active_jobs enforcement ────────────────────────────────────
117
+ createJob(input) {
118
+ const cfg = ConfigManager.getInstance().getChronosConfig();
119
+ const activeCount = this.db.prepare(`SELECT COUNT(*) as cnt FROM chronos_jobs WHERE enabled = 1`).get().cnt;
120
+ if (activeCount >= cfg.max_active_jobs) {
121
+ throw new ChronosError(`Maximum active jobs limit (${cfg.max_active_jobs}) reached. Disable or delete an existing job first.`);
122
+ }
123
+ const now = Date.now();
124
+ const id = randomUUID();
125
+ this.db.prepare(`
126
+ INSERT INTO chronos_jobs (
127
+ id, prompt, schedule_type, schedule_expression, cron_normalized,
128
+ timezone, next_run_at, last_run_at, enabled, created_at, updated_at, created_by
129
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 1, ?, ?, ?)
130
+ `).run(id, input.prompt, input.schedule_type, input.schedule_expression, input.cron_normalized ?? null, input.timezone, input.next_run_at, now, now, input.created_by);
131
+ return this.getJob(id);
132
+ }
133
+ getJob(id) {
134
+ const row = this.db.prepare('SELECT * FROM chronos_jobs WHERE id = ?').get(id);
135
+ return row ? this.deserializeJob(row) : null;
136
+ }
137
+ listJobs(filters) {
138
+ const params = [];
139
+ let query = 'SELECT * FROM chronos_jobs WHERE 1=1';
140
+ if (filters?.enabled !== undefined) {
141
+ query += ' AND enabled = ?';
142
+ params.push(filters.enabled ? 1 : 0);
143
+ }
144
+ if (filters?.created_by) {
145
+ query += ' AND created_by = ?';
146
+ params.push(filters.created_by);
147
+ }
148
+ query += ' ORDER BY created_at DESC';
149
+ const rows = this.db.prepare(query).all(...params);
150
+ return rows.map((r) => this.deserializeJob(r));
151
+ }
152
+ updateJob(id, patch) {
153
+ const now = Date.now();
154
+ const sets = ['updated_at = ?'];
155
+ const params = [now];
156
+ if (patch.prompt !== undefined) {
157
+ sets.push('prompt = ?');
158
+ params.push(patch.prompt);
159
+ }
160
+ if (patch.schedule_expression !== undefined) {
161
+ sets.push('schedule_expression = ?');
162
+ params.push(patch.schedule_expression);
163
+ }
164
+ if ('cron_normalized' in patch) {
165
+ sets.push('cron_normalized = ?');
166
+ params.push(patch.cron_normalized ?? null);
167
+ }
168
+ if (patch.timezone !== undefined) {
169
+ sets.push('timezone = ?');
170
+ params.push(patch.timezone);
171
+ }
172
+ if ('next_run_at' in patch) {
173
+ sets.push('next_run_at = ?');
174
+ params.push(patch.next_run_at ?? null);
175
+ }
176
+ if ('last_run_at' in patch) {
177
+ sets.push('last_run_at = ?');
178
+ params.push(patch.last_run_at ?? null);
179
+ }
180
+ if (patch.enabled !== undefined) {
181
+ sets.push('enabled = ?');
182
+ params.push(patch.enabled ? 1 : 0);
183
+ }
184
+ params.push(id);
185
+ this.db.prepare(`UPDATE chronos_jobs SET ${sets.join(', ')} WHERE id = ?`).run(...params);
186
+ return this.getJob(id);
187
+ }
188
+ deleteJob(id) {
189
+ const result = this.db.prepare('DELETE FROM chronos_jobs WHERE id = ?').run(id);
190
+ return result.changes > 0;
191
+ }
192
+ getDueJobs(nowMs) {
193
+ const rows = this.db.prepare(`
194
+ SELECT * FROM chronos_jobs
195
+ WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= ?
196
+ ORDER BY next_run_at ASC
197
+ `).all(nowMs);
198
+ return rows.map((r) => this.deserializeJob(r));
199
+ }
200
+ enableJob(id) {
201
+ const now = Date.now();
202
+ this.db.prepare(`UPDATE chronos_jobs SET enabled = 1, updated_at = ? WHERE id = ?`).run(now, id);
203
+ return this.getJob(id);
204
+ }
205
+ disableJob(id) {
206
+ const now = Date.now();
207
+ this.db.prepare(`UPDATE chronos_jobs SET enabled = 0, next_run_at = NULL, updated_at = ? WHERE id = ?`).run(now, id);
208
+ return this.getJob(id);
209
+ }
210
+ // ─── Executions ───────────────────────────────────────────────────────────
211
+ insertExecution(record) {
212
+ this.db.prepare(`
213
+ INSERT INTO chronos_executions (id, job_id, triggered_at, completed_at, status, error, session_id)
214
+ VALUES (?, ?, ?, NULL, ?, NULL, ?)
215
+ `).run(record.id, record.job_id, record.triggered_at, record.status, record.session_id);
216
+ }
217
+ completeExecution(id, status, error) {
218
+ const now = Date.now();
219
+ this.db.prepare(`
220
+ UPDATE chronos_executions SET status = ?, completed_at = ?, error = ? WHERE id = ?
221
+ `).run(status, now, error ?? null, id);
222
+ }
223
+ listExecutions(jobId, limit = 50) {
224
+ const rows = this.db.prepare(`
225
+ SELECT * FROM chronos_executions WHERE job_id = ?
226
+ ORDER BY triggered_at DESC LIMIT ?
227
+ `).all(jobId, Math.min(limit, 100));
228
+ return rows.map((r) => this.deserializeExecution(r));
229
+ }
230
+ pruneExecutions(jobId, keepCount) {
231
+ this.db.prepare(`
232
+ DELETE FROM chronos_executions
233
+ WHERE job_id = ? AND id NOT IN (
234
+ SELECT id FROM chronos_executions
235
+ WHERE job_id = ?
236
+ ORDER BY triggered_at DESC
237
+ LIMIT ?
238
+ )
239
+ `).run(jobId, jobId, keepCount);
240
+ }
241
+ close() {
242
+ this.db.close();
243
+ }
244
+ }
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { ConfigManager } from '../../config/manager.js';
3
+ import { DisplayManager } from '../display.js';
4
+ import { parseNextRun } from './parser.js';
5
+ export class ChronosWorker {
6
+ repo;
7
+ oracle;
8
+ static instance = null;
9
+ static notifyFn = null;
10
+ /**
11
+ * True while a Chronos job is being executed. Chronos management tools
12
+ * (chronos_cancel, chronos_schedule) check this flag and refuse to operate
13
+ * during execution to prevent the Oracle from self-deleting or re-scheduling
14
+ * the active job.
15
+ */
16
+ static isExecuting = false;
17
+ timer = null;
18
+ isRunning = false;
19
+ pollIntervalMs;
20
+ constructor(repo, oracle) {
21
+ this.repo = repo;
22
+ this.oracle = oracle;
23
+ this.pollIntervalMs = ConfigManager.getInstance().getChronosConfig().check_interval_ms;
24
+ }
25
+ static getInstance() {
26
+ return ChronosWorker.instance;
27
+ }
28
+ static setInstance(worker) {
29
+ ChronosWorker.instance = worker;
30
+ }
31
+ /** Register a function that will deliver Oracle responses to users (e.g. Telegram). */
32
+ static setNotifyFn(fn) {
33
+ ChronosWorker.notifyFn = fn;
34
+ }
35
+ start() {
36
+ if (this.timer)
37
+ return;
38
+ const display = DisplayManager.getInstance();
39
+ display.log(`Worker started (interval: ${this.pollIntervalMs}ms)`, { source: 'Chronos' });
40
+ this.timer = setInterval(() => void this.tick(), this.pollIntervalMs);
41
+ }
42
+ stop() {
43
+ if (this.timer) {
44
+ clearInterval(this.timer);
45
+ this.timer = null;
46
+ }
47
+ const display = DisplayManager.getInstance();
48
+ display.log('Worker stopped', { source: 'Chronos' });
49
+ }
50
+ /** Hot-reload poll interval without restarting the process */
51
+ updateInterval(newMs) {
52
+ if (newMs < 60000)
53
+ return;
54
+ this.pollIntervalMs = newMs;
55
+ if (this.timer) {
56
+ clearInterval(this.timer);
57
+ this.timer = setInterval(() => void this.tick(), this.pollIntervalMs);
58
+ }
59
+ const display = DisplayManager.getInstance();
60
+ display.log(`Worker interval updated to ${newMs}ms`, { source: 'Chronos' });
61
+ }
62
+ async tick() {
63
+ if (this.isRunning)
64
+ return;
65
+ this.isRunning = true;
66
+ try {
67
+ const dueJobs = this.repo.getDueJobs(Date.now());
68
+ for (const job of dueJobs) {
69
+ void this.executeJob(job);
70
+ }
71
+ }
72
+ finally {
73
+ this.isRunning = false;
74
+ }
75
+ }
76
+ async executeJob(job) {
77
+ const display = DisplayManager.getInstance();
78
+ const execId = randomUUID();
79
+ display.log(`Job ${job.id} triggered — "${job.prompt.slice(0, 60)}"`, { source: 'Chronos' });
80
+ // Use the currently active Oracle session so Chronos executes in the
81
+ // user's conversation context — no session switching or isolation needed.
82
+ const activeSessionId = this.oracle.getCurrentSessionId() ?? 'default';
83
+ this.repo.insertExecution({
84
+ id: execId,
85
+ job_id: job.id,
86
+ triggered_at: Date.now(),
87
+ status: 'running',
88
+ session_id: activeSessionId,
89
+ });
90
+ try {
91
+ // Inject execution context as an AI message so it appears naturally in the
92
+ // conversation history without triggering an extra LLM response.
93
+ const contextMessage = `[CHRONOS EXECUTION — job_id: ${job.id}]\n` +
94
+ `Executing scheduled job. Do NOT call chronos_cancel, chronos_schedule, ` +
95
+ `or any Chronos management tools during this execution.`;
96
+ await this.oracle.injectAIMessage(contextMessage);
97
+ // If a Telegram notify function is registered, tag delegated tasks with
98
+ // origin_channel: 'telegram' so the TaskDispatcher broadcasts their result.
99
+ const taskContext = ChronosWorker.notifyFn
100
+ ? { origin_channel: 'telegram', session_id: activeSessionId }
101
+ : undefined;
102
+ // Hard-block Chronos management tools during execution.
103
+ ChronosWorker.isExecuting = true;
104
+ const response = await this.oracle.chat(job.prompt, undefined, false, taskContext);
105
+ this.repo.completeExecution(execId, 'success');
106
+ display.log(`Job ${job.id} completed — status: success`, { source: 'Chronos' });
107
+ // Deliver Oracle response to notification channels.
108
+ await this.notify(job, response);
109
+ }
110
+ catch (err) {
111
+ const errMsg = err?.message ?? String(err);
112
+ this.repo.completeExecution(execId, 'failed', errMsg);
113
+ display.log(`Job ${job.id} failed — ${errMsg}`, { source: 'Chronos', level: 'error' });
114
+ }
115
+ finally {
116
+ ChronosWorker.isExecuting = false;
117
+ if (job.schedule_type === 'once') {
118
+ this.repo.disableJob(job.id);
119
+ display.log(`Job ${job.id} auto-disabled (once-type)`, { source: 'Chronos' });
120
+ }
121
+ else if (job.cron_normalized) {
122
+ const nextRunAt = parseNextRun(job.cron_normalized, job.timezone);
123
+ this.repo.updateJob(job.id, { next_run_at: nextRunAt, last_run_at: Date.now() });
124
+ display.log(`Job ${job.id} rescheduled — next_run_at: ${new Date(nextRunAt).toISOString()}`, { source: 'Chronos' });
125
+ }
126
+ this.repo.pruneExecutions(job.id, 100);
127
+ }
128
+ }
129
+ async notify(job, response) {
130
+ if (!ChronosWorker.notifyFn)
131
+ return;
132
+ const display = DisplayManager.getInstance();
133
+ const header = `⏰ *Chronos* — _${job.prompt.slice(0, 80)}${job.prompt.length > 80 ? '…' : ''}_\n\n`;
134
+ try {
135
+ await ChronosWorker.notifyFn(header + response);
136
+ }
137
+ catch (err) {
138
+ display.log(`Job ${job.id} notification failed — ${err.message}`, { source: 'Chronos', level: 'error' });
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ChronosWorker } from './worker.js';
3
+ // Mock ConfigManager and DisplayManager
4
+ vi.mock('../../config/manager.js', () => ({
5
+ ConfigManager: {
6
+ getInstance: () => ({
7
+ getChronosConfig: () => ({ timezone: 'UTC', check_interval_ms: 60000, max_active_jobs: 100 }),
8
+ }),
9
+ },
10
+ }));
11
+ vi.mock('../display.js', () => ({
12
+ DisplayManager: {
13
+ getInstance: () => ({
14
+ log: vi.fn(),
15
+ }),
16
+ },
17
+ }));
18
+ function makeJob(overrides = {}) {
19
+ return {
20
+ id: 'job-1',
21
+ prompt: 'Say hello',
22
+ schedule_type: 'once',
23
+ schedule_expression: 'in 1 hour',
24
+ cron_normalized: null,
25
+ timezone: 'UTC',
26
+ next_run_at: Date.now() - 1000,
27
+ last_run_at: null,
28
+ enabled: true,
29
+ created_at: Date.now() - 5000,
30
+ updated_at: Date.now() - 5000,
31
+ created_by: 'ui',
32
+ ...overrides,
33
+ };
34
+ }
35
+ function makeRepo(jobs = []) {
36
+ return {
37
+ getDueJobs: vi.fn().mockReturnValue(jobs),
38
+ insertExecution: vi.fn(),
39
+ completeExecution: vi.fn(),
40
+ disableJob: vi.fn(),
41
+ enableJob: vi.fn(),
42
+ updateJob: vi.fn(),
43
+ pruneExecutions: vi.fn(),
44
+ createJob: vi.fn(),
45
+ getJob: vi.fn(),
46
+ listJobs: vi.fn(),
47
+ deleteJob: vi.fn(),
48
+ listExecutions: vi.fn(),
49
+ close: vi.fn(),
50
+ };
51
+ }
52
+ function makeOracle() {
53
+ return {
54
+ chat: vi.fn().mockResolvedValue('Oracle response'),
55
+ setSessionId: vi.fn().mockResolvedValue(undefined),
56
+ getCurrentSessionId: vi.fn().mockReturnValue('user-session-1'),
57
+ initialize: vi.fn(),
58
+ getHistory: vi.fn(),
59
+ createNewSession: vi.fn(),
60
+ clearMemory: vi.fn(),
61
+ reloadTools: vi.fn(),
62
+ };
63
+ }
64
+ describe('ChronosWorker.tick()', () => {
65
+ it('isRunning guard prevents concurrent execution', async () => {
66
+ const repo = makeRepo([]);
67
+ const oracle = makeOracle();
68
+ const worker = new ChronosWorker(repo, oracle);
69
+ // Set isRunning to true via tick (accessing private via casting)
70
+ worker.isRunning = true;
71
+ await worker.tick();
72
+ expect(repo.getDueJobs).not.toHaveBeenCalled();
73
+ });
74
+ it('calls getDueJobs with current timestamp', async () => {
75
+ const repo = makeRepo([]);
76
+ const oracle = makeOracle();
77
+ const worker = new ChronosWorker(repo, oracle);
78
+ const before = Date.now();
79
+ await worker.tick();
80
+ const after = Date.now();
81
+ const [[ts]] = repo.getDueJobs.mock.calls;
82
+ expect(ts).toBeGreaterThanOrEqual(before);
83
+ expect(ts).toBeLessThanOrEqual(after);
84
+ });
85
+ it('triggers oracle.chat for a once-type due job then disables it', async () => {
86
+ const job = makeJob({ schedule_type: 'once' });
87
+ const repo = makeRepo([job]);
88
+ const oracle = makeOracle();
89
+ const worker = new ChronosWorker(repo, oracle);
90
+ await worker.tick();
91
+ // Wait for fire-and-forget
92
+ await new Promise((r) => setTimeout(r, 50));
93
+ expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt));
94
+ expect(repo.disableJob).toHaveBeenCalledWith(job.id);
95
+ expect(repo.updateJob).not.toHaveBeenCalled();
96
+ });
97
+ it('triggers oracle.chat for a recurring job then updates next_run_at', async () => {
98
+ const job = makeJob({ schedule_type: 'cron', cron_normalized: '0 9 * * *' });
99
+ const repo = makeRepo([job]);
100
+ const oracle = makeOracle();
101
+ const worker = new ChronosWorker(repo, oracle);
102
+ await worker.tick();
103
+ await new Promise((r) => setTimeout(r, 50));
104
+ expect(oracle.chat).toHaveBeenCalledWith(expect.stringContaining(job.prompt));
105
+ expect(repo.disableJob).not.toHaveBeenCalled();
106
+ expect(repo.updateJob).toHaveBeenCalledWith(job.id, expect.objectContaining({ next_run_at: expect.any(Number) }));
107
+ });
108
+ it('sets execution status to failed when oracle.chat rejects', async () => {
109
+ const job = makeJob({ schedule_type: 'once' });
110
+ const repo = makeRepo([job]);
111
+ const oracle = makeOracle();
112
+ oracle.chat.mockRejectedValue(new Error('LLM error'));
113
+ const worker = new ChronosWorker(repo, oracle);
114
+ await worker.tick();
115
+ await new Promise((r) => setTimeout(r, 50));
116
+ expect(repo.completeExecution).toHaveBeenCalledWith(expect.any(String), 'failed', 'LLM error');
117
+ // For once-type, still auto-disable even on failure
118
+ expect(repo.disableJob).toHaveBeenCalledWith(job.id);
119
+ });
120
+ });
@@ -98,6 +98,9 @@ export class DisplayManager {
98
98
  else if (options.source === 'Zaion') {
99
99
  color = chalk.hex('#00c3ff');
100
100
  }
101
+ else if (options.source === 'Chronos') {
102
+ color = chalk.hex('#a855f7');
103
+ }
101
104
  prefix = color(`[${options.source}] `);
102
105
  }
103
106
  let formattedMessage = message;
@@ -208,7 +208,7 @@ export class SatiRepository {
208
208
  searchUnifiedVector(embedding, limit) {
209
209
  if (!this.db)
210
210
  return [];
211
- const SIMILARITY_THRESHOLD = 0.8;
211
+ const SIMILARITY_THRESHOLD = 0.9;
212
212
  const stmt = this.db.prepare(`
213
213
  SELECT *
214
214
  FROM (