morpheus-cli 0.9.0 → 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.
Files changed (63) hide show
  1. package/README.md +18 -4
  2. package/dist/channels/discord.js +133 -6
  3. package/dist/channels/telegram.js +23 -17
  4. package/dist/config/manager.js +11 -0
  5. package/dist/config/schemas.js +5 -0
  6. package/dist/http/api.js +5 -3
  7. package/dist/http/routers/danger.js +137 -0
  8. package/dist/runtime/apoc.js +1 -1
  9. package/dist/runtime/audit/repository.js +2 -0
  10. package/dist/runtime/keymaker.js +1 -1
  11. package/dist/runtime/memory/sati/index.js +1 -1
  12. package/dist/runtime/memory/sati/service.js +28 -1
  13. package/dist/runtime/memory/session-embedding-worker.js +43 -36
  14. package/dist/runtime/memory/sqlite.js +31 -124
  15. package/dist/runtime/neo.js +1 -1
  16. package/dist/runtime/oracle.js +55 -54
  17. package/dist/runtime/setup/__tests__/repository.test.js +115 -0
  18. package/dist/runtime/setup/repository.js +87 -0
  19. package/dist/runtime/smiths/delegator.js +1 -1
  20. package/dist/runtime/tools/setup-tool.js +57 -0
  21. package/dist/runtime/trinity.js +1 -1
  22. package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +1 -0
  23. package/dist/ui/assets/Chat-5AeRYuRj.js +41 -0
  24. package/dist/ui/assets/{Chronos-BAjeLobF.js → Chronos-BrKldYVw.js} +1 -1
  25. package/dist/ui/assets/{ConfirmationModal-fvgnOWTY.js → ConfirmationModal-DsbS3XkJ.js} +1 -1
  26. package/dist/ui/assets/{Dashboard-Ca5mSefz.js → Dashboard-DvrTXLdo.js} +1 -1
  27. package/dist/ui/assets/{DeleteConfirmationModal-A8EmnHoa.js → DeleteConfirmationModal-BfSjv04R.js} +1 -1
  28. package/dist/ui/assets/{Logs-CYu7se7R.js → Logs-B0ZYWs5x.js} +1 -1
  29. package/dist/ui/assets/MCPManager-BwHGTeNs.js +1 -0
  30. package/dist/ui/assets/{ModelPricing-DnSm_Nh-.js → ModelPricing-CYhGRQr8.js} +1 -1
  31. package/dist/ui/assets/{Notifications-CiljQzvM.js → Notifications-BYMAtVMq.js} +1 -1
  32. package/dist/ui/assets/{Pagination-JsiwxVNQ.js → Pagination-oTGieBLM.js} +1 -1
  33. package/dist/ui/assets/SatiMemories-I1vsYtP2.js +1 -0
  34. package/dist/ui/assets/SessionAudit-BCecQWde.js +9 -0
  35. package/dist/ui/assets/Settings-Cu4D-7tb.js +47 -0
  36. package/dist/ui/assets/Skills-lGU3I5DO.js +7 -0
  37. package/dist/ui/assets/Smiths-DnEH3nID.js +1 -0
  38. package/dist/ui/assets/Tasks-Bz92GPWK.js +1 -0
  39. package/dist/ui/assets/{TrinityDatabases-BzYfecKI.js → TrinityDatabases-BUY-3j7Q.js} +1 -1
  40. package/dist/ui/assets/{UsageStats-CBo2vW2n.js → UsageStats-Dr5eSgJc.js} +1 -1
  41. package/dist/ui/assets/{WebhookManager-0tDFkfHd.js → WebhookManager-DIASAC-1.js} +1 -1
  42. package/dist/ui/assets/{audit-B-F8XPLi.js → audit-CcAEDbZh.js} +1 -1
  43. package/dist/ui/assets/{chronos-BvMxfBQH.js → chronos-2Z9E96_1.js} +1 -1
  44. package/dist/ui/assets/{config-DteVgNGR.js → config-DdfK4DX6.js} +1 -1
  45. package/dist/ui/assets/index-D4fzIKy1.css +1 -0
  46. package/dist/ui/assets/{index-Cwqr-n0Y.js → index-Dpd1Mkgp.js} +5 -5
  47. package/dist/ui/assets/{mcp-DxzodOdH.js → mcp-BWMt8aY7.js} +1 -1
  48. package/dist/ui/assets/{skills--hAyQnmG.js → skills-D7JjK7JH.js} +1 -1
  49. package/dist/ui/assets/{stats-Cibaisqd.js → stats-DoIhtLot.js} +1 -1
  50. package/dist/ui/assets/{vendor-icons-BVuQI-6R.js → vendor-icons-DMd9RGvJ.js} +1 -1
  51. package/dist/ui/index.html +3 -3
  52. package/dist/ui/sw.js +1 -1
  53. package/package.json +5 -4
  54. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +0 -1
  55. package/dist/ui/assets/Chat-CjxeAQmd.js +0 -41
  56. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +0 -1
  57. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +0 -1
  58. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +0 -9
  59. package/dist/ui/assets/Settings-OQlHAJoy.js +0 -41
  60. package/dist/ui/assets/Skills-Crsybug0.js +0 -7
  61. package/dist/ui/assets/Smiths-wm90jRDT.js +0 -1
  62. package/dist/ui/assets/Tasks-C5FMu_Yu.js +0 -1
  63. package/dist/ui/assets/index-DcfyUdLI.css +0 -1
@@ -97,7 +97,7 @@ export class SatiService {
97
97
  console.warn('[SatiService] Failed to persist input log:', e);
98
98
  }
99
99
  const satiStartMs = Date.now();
100
- const response = await agent.invoke({ messages }, { recursionLimit: 100 });
100
+ const response = await agent.invoke({ messages }, { recursionLimit: 50 });
101
101
  const satiDurationMs = Date.now() - satiStartMs;
102
102
  const lastMessage = response.messages[response.messages.length - 1];
103
103
  let content = lastMessage.content.toString();
@@ -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
- while (true) {
22
- const sessions = shortDb.prepare(`
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
- if (sessions.length === 0) {
30
- // display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
31
- break;
32
- }
33
- for (const session of sessions) {
34
- const sessionId = session.id;
35
- display.log(`🧠 Processando sessão ${sessionId}...`, { source: 'SessionEmbeddingWorker' });
36
- try {
37
- // Skip setting 'processing' as it violates CHECK constraint
38
- // active_processing.add(sessionId); // If we needed concurrency control
39
- const chunks = satiDb.prepare(`
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
- if (chunks.length === 0) {
46
- display.log(`⚠️ Sessão ${sessionId} não possui chunks.`, { source: 'SessionEmbeddingWorker' });
47
- shortDb.prepare(`
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
- continue;
54
- }
55
- const insertVec = satiDb.prepare(`
54
+ continue;
55
+ }
56
+ const insertVec = satiDb.prepare(`
56
57
  INSERT INTO session_vec (embedding)
57
58
  VALUES (?)
58
59
  `);
59
- const insertMap = satiDb.prepare(`
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
- for (const chunk of chunks) {
65
- display.log(` ↳ Embedding chunk ${chunk.id}`, { source: 'SessionEmbeddingWorker' });
66
- const embedding = await embeddingService.generate(chunk.content);
67
- if (!embedding || embedding.length !== EMBEDDING_DIM) {
68
- throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
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
- const result = insertVec.run(new Float32Array(embedding));
71
- const vecRowId = result.lastInsertRowid;
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
- display.log(`✅ Sessão ${sessionId} embedada com sucesso.`, { source: 'SessionEmbeddingWorker' });
82
- }
83
- catch (err) {
84
- display.log(`❌ Erro na sessão ${sessionId}: ${err}`, { source: 'SessionEmbeddingWorker' });
85
- shortDb.prepare(`
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
  }
@@ -716,36 +716,19 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
716
716
  }
717
717
  async createNewSession() {
718
718
  const now = Date.now();
719
- // Transação para garantir consistência
720
- const tx = this.db.transaction(() => {
721
- // Pegar a sessão atualmente ativa
722
- const activeSession = this.db.prepare(`
723
- SELECT id FROM sessions
724
- WHERE status = 'active'
725
- `).get();
726
- // Se houver uma sessão ativa, mudar seu status para 'paused'
727
- if (activeSession) {
728
- this.db.prepare(`
729
- UPDATE sessions
730
- SET status = 'paused'
731
- WHERE id = ?
732
- `).run(activeSession.id);
733
- }
734
- // Criar uma nova sessão ativa
735
- const newId = randomUUID();
736
- this.db.prepare(`
737
- INSERT INTO sessions (
738
- id,
739
- started_at,
740
- status
741
- ) VALUES (?, ?, 'active')
742
- `).run(newId, now);
743
- // Atualizar o ID da sessão atual desta instância
744
- this.sessionId = newId;
745
- this.titleSet = false; // reset cache for new session
746
- });
747
- tx(); // Executar a transação
748
- this.display.log('✅ Nova sessão iniciada e sessão anterior pausada', { source: 'Sati' });
719
+ const newId = randomUUID();
720
+ this.db.prepare(`
721
+ INSERT INTO sessions (
722
+ id,
723
+ started_at,
724
+ status
725
+ ) VALUES (?, ?, 'active')
726
+ `).run(newId, now);
727
+ // Update this instance to point to the new session
728
+ this.sessionId = newId;
729
+ this.titleSet = false;
730
+ this.display.log('✅ New session created', { source: 'Sati' });
731
+ return newId;
749
732
  }
750
733
  chunkText(text, chunkSize = 500, overlap = 50) {
751
734
  if (!text || text.length === 0)
@@ -890,27 +873,6 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
890
873
  `).run(now, sessionId);
891
874
  });
892
875
  tx(); // Executar a transação
893
- // Se a sessão era active, verificar se há outra para ativar
894
- if (session.status === 'active') {
895
- const nextSession = this.db.prepare(`
896
- SELECT id FROM sessions
897
- WHERE status = 'paused'
898
- ORDER BY started_at DESC
899
- LIMIT 1
900
- `).get();
901
- if (nextSession) {
902
- // Promover a próxima sessão a ativa
903
- this.db.prepare(`
904
- UPDATE sessions
905
- SET status = 'active'
906
- WHERE id = ?
907
- `).run(nextSession.id);
908
- }
909
- else {
910
- // Nenhuma outra sessão, criar nova
911
- this.createFreshSession();
912
- }
913
- }
914
876
  }
915
877
  /**
916
878
  * Renomear uma sessão ativa ou pausada.
@@ -941,101 +903,46 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
941
903
  tx(); // Executar a transação
942
904
  }
943
905
  /**
944
- * Trocar o contexto ativo entre sessões não finalizadas.
945
- * Validar sessão alvo: existe e status ∈ (paused, active).
946
- * Se já for active, não faz nada.
947
- * Transação: sessão atual active → paused, sessão alvo → active.
948
- */
949
- /**
950
- * Creates a session row with status 'paused' if it doesn't already exist.
906
+ * Creates a session row with status 'active' if it doesn't already exist.
951
907
  * Safe to call multiple times — idempotent.
952
908
  */
953
909
  ensureSession(sessionId) {
954
910
  const existing = this.db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
955
911
  if (!existing) {
956
- this.db.prepare("INSERT INTO sessions (id, started_at, status) VALUES (?, ?, 'paused')").run(sessionId, Date.now());
912
+ this.db.prepare("INSERT INTO sessions (id, started_at, status) VALUES (?, ?, 'active')").run(sessionId, Date.now());
957
913
  }
958
914
  }
915
+ /**
916
+ * Validates that the target session exists and is usable (not archived/deleted).
917
+ * No longer swaps active↔paused — sessions are independently usable from any channel.
918
+ */
959
919
  async switchSession(targetSessionId) {
960
- // Validar sessão alvo: existe e status ∈ (paused, active)
961
920
  const targetSession = this.db.prepare(`
962
921
  SELECT id, status FROM sessions
963
922
  WHERE id = ?
964
923
  `).get(targetSessionId);
965
924
  if (!targetSession) {
966
- throw new Error(`Sessão alvo com ID ${targetSessionId} não encontrada.`);
925
+ throw new Error(`Session with ID ${targetSessionId} not found.`);
967
926
  }
968
- if (targetSession.status !== 'active' && targetSession.status !== 'paused') {
969
- throw new Error(`Sessão alvo com ID ${targetSessionId} não está em estado ativo ou pausado. Status atual: ${targetSession.status}`);
927
+ if (targetSession.status === 'archived' || targetSession.status === 'deleted') {
928
+ throw new Error(`Session ${targetSessionId} is ${targetSession.status} and cannot be used.`);
970
929
  }
971
- // Se já for active, não faz nada
972
- if (targetSession.status === 'active') {
973
- return; // A sessão alvo já está ativa, não precisa fazer nada
974
- }
975
- // Transação: sessão atual active → paused, sessão alvo → active
976
- const tx = this.db.transaction(() => {
977
- // Pegar a sessão atualmente ativa
978
- const currentActiveSession = this.db.prepare(`
979
- SELECT id FROM sessions
980
- WHERE status = 'active'
981
- `).get();
982
- // Se houver uma sessão ativa, mudar seu status para 'paused'
983
- if (currentActiveSession) {
984
- this.db.prepare(`
985
- UPDATE sessions
986
- SET status = 'paused'
987
- WHERE id = ?
988
- `).run(currentActiveSession.id);
989
- }
990
- // Mudar o status da sessão alvo para 'active'
991
- this.db.prepare(`
992
- UPDATE sessions
993
- SET status = 'active'
994
- WHERE id = ?
995
- `).run(targetSessionId);
996
- });
997
- tx(); // Executar a transação
998
930
  }
999
931
  /**
1000
- * Garantir que sempre exista uma sessão ativa válida.
1001
- * Buscar sessão com status = 'active', retornar seu id se existir,
1002
- * ou criar nova sessão (createFreshSession) e retornar o novo id.
932
+ * Returns the most recently created usable session, or creates one if none exist.
933
+ * A session is usable if its status is 'active' or 'paused' (both are equivalent post-refactor).
1003
934
  */
1004
935
  async getCurrentSessionOrCreate() {
1005
- // Buscar sessão com status = 'active'
1006
- const activeSession = this.db.prepare(`
1007
- SELECT id FROM sessions
1008
- WHERE status = 'active'
1009
- `).get();
1010
- if (activeSession) {
1011
- // Se existir, retornar seu id
1012
- return activeSession.id;
1013
- }
1014
- else {
1015
- // Se não existir, criar nova sessão (createFreshSession) e retornar o novo id
1016
- const newId = await this.createFreshSession();
1017
- return newId;
1018
- }
1019
- }
1020
- async createFreshSession() {
1021
- // Validar que não existe sessão 'active'
1022
- const activeSession = this.db.prepare(`
936
+ const session = this.db.prepare(`
1023
937
  SELECT id FROM sessions
1024
- WHERE status = 'active'
938
+ WHERE status IN ('active', 'paused')
939
+ ORDER BY started_at DESC
940
+ LIMIT 1
1025
941
  `).get();
1026
- if (activeSession) {
1027
- throw new Error('Já existe uma sessão ativa. Não é possível criar uma nova sessão ativa.');
942
+ if (session) {
943
+ return session.id;
1028
944
  }
1029
- const now = Date.now();
1030
- const newId = randomUUID();
1031
- this.db.prepare(`
1032
- INSERT INTO sessions (
1033
- id,
1034
- started_at,
1035
- status
1036
- ) VALUES (?, ?, 'active')
1037
- `).run(newId, now);
1038
- return newId;
945
+ return this.createNewSession();
1039
946
  }
1040
947
  /**
1041
948
  * Lists all active and paused sessions with their basic information.
@@ -137,7 +137,7 @@ ${context ? `Context:\n${context}` : ""}
137
137
  };
138
138
  const inputCount = messages.length;
139
139
  const startMs = Date.now();
140
- const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 100 }));
140
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 50 }));
141
141
  const durationMs = Date.now() - startMs;
142
142
  const lastMessage = response.messages[response.messages.length - 1];
143
143
  const content = typeof lastMessage.content === "string"
@@ -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(),
@@ -208,6 +213,8 @@ export class Oracle {
208
213
  if (!this.history) {
209
214
  throw new Error("Message history not initialized. Call initialize() first.");
210
215
  }
216
+ // Per-call scoped history — declared outside try so finally can close it.
217
+ let callHistory;
211
218
  try {
212
219
  this.display.log('Processing message...', { source: 'Oracle' });
213
220
  const userMessage = new HumanMessage(message);
@@ -220,8 +227,25 @@ export class Oracle {
220
227
  if (extraUsage) {
221
228
  userMessage.usage_metadata = extraUsage;
222
229
  }
223
- const systemMessage = new SystemMessage(`
224
- You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
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.
225
249
 
226
250
  You are an orchestrator and task router.
227
251
 
@@ -347,13 +371,20 @@ bad:
347
371
  ${SkillRegistry.getInstance().getSystemPromptSection()}
348
372
  ${SmithRegistry.getInstance().getSystemPromptSection()}
349
373
  `);
374
+ // Resolve the authoritative session ID for this call.
375
+ // Priority: explicit taskContext > current history instance > fallback.
376
+ const currentSessionId = taskContext?.session_id
377
+ ?? ((this.history instanceof SQLiteChatMessageHistory) ? this.history.currentSessionId : undefined);
378
+ // Create a per-call scoped history so concurrent chat() calls for
379
+ // different sessions never interfere with each other.
380
+ callHistory = new SQLiteChatMessageHistory({
381
+ sessionId: currentSessionId ?? 'default',
382
+ databasePath: this.databasePath,
383
+ limit: this.config.llm?.context_window ?? 100,
384
+ });
350
385
  // Load existing history from database in reverse order (most recent first)
351
- let previousMessages = await this.history.getMessages();
386
+ let previousMessages = await callHistory.getMessages();
352
387
  previousMessages = previousMessages.reverse();
353
- // Propagate current session to Apoc so its token usage lands in the right session
354
- const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
355
- ? this.history.currentSessionId
356
- : undefined;
357
388
  // Sati Middleware: Retrieval
358
389
  let memoryMessage = null;
359
390
  try {
@@ -395,7 +426,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
395
426
  let syncDelegationCount = 0;
396
427
  const oracleStartMs = Date.now();
397
428
  const response = await TaskRequestContext.run(invokeContext, async () => {
398
- const agentResponse = await this.provider.invoke({ messages }, { recursionLimit: 100 });
429
+ const agentResponse = await this.provider.invoke({ messages }, { recursionLimit: 50 });
399
430
  contextDelegationAcks = TaskRequestContext.getDelegationAcks();
400
431
  syncDelegationCount = TaskRequestContext.getSyncDelegationCount();
401
432
  return agentResponse;
@@ -474,8 +505,8 @@ Use it to inform your response and tool selection (if needed), but do not assume
474
505
  ackMessage.usage_metadata = ackResult.usage_metadata;
475
506
  }
476
507
  // Persist with addMessage so ack-provider usage is tracked per message row.
477
- await this.history.addMessage(userMessage);
478
- await this.history.addMessage(ackMessage);
508
+ await callHistory.addMessage(userMessage);
509
+ await callHistory.addMessage(ackMessage);
479
510
  // Unblock tasks for execution: the ack message is now persisted and will be
480
511
  // returned to the caller (Telegram / UI) immediately after this point.
481
512
  this.taskRepository.markAckSent(validDelegationAcks.map(a => a.task_id));
@@ -489,7 +520,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
489
520
  provider: this.config.llm.provider,
490
521
  model: this.config.llm.model,
491
522
  };
492
- await this.history.addMessages([userMessage, failureMessage]);
523
+ await callHistory.addMessages([userMessage, failureMessage]);
493
524
  }
494
525
  else {
495
526
  const lastMessage = response.messages[response.messages.length - 1];
@@ -510,11 +541,11 @@ Use it to inform your response and tool selection (if needed), but do not assume
510
541
  if (usage) {
511
542
  failureMessage.usage_metadata = usage;
512
543
  }
513
- await this.history.addMessages([userMessage, failureMessage]);
544
+ await callHistory.addMessages([userMessage, failureMessage]);
514
545
  }
515
546
  else {
516
547
  // Persist user message + all generated messages in a single transaction
517
- await this.history.addMessages([userMessage, ...newGeneratedMessages]);
548
+ await callHistory.addMessages([userMessage, ...newGeneratedMessages]);
518
549
  }
519
550
  }
520
551
  this.display.log('Response generated.', { source: 'Oracle' });
@@ -539,6 +570,9 @@ Use it to inform your response and tool selection (if needed), but do not assume
539
570
  catch (err) {
540
571
  throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
541
572
  }
573
+ finally {
574
+ callHistory?.close();
575
+ }
542
576
  }
543
577
  async getHistory() {
544
578
  if (!this.history) {
@@ -558,53 +592,19 @@ Use it to inform your response and tool selection (if needed), but do not assume
558
592
  throw new Error("Current history provider does not support session rollover.");
559
593
  }
560
594
  }
595
+ /**
596
+ * Updates the internal history pointer to the given session.
597
+ * No longer mutates DB session status — sessions are independently usable from any channel.
598
+ * Note: chat() uses per-call callHistory scoped to taskContext.session_id,
599
+ * so this method is only a fallback for callers that don't pass session in taskContext.
600
+ */
561
601
  async setSessionId(sessionId) {
562
602
  if (!this.history) {
563
603
  throw new Error("Message history not initialized. Call initialize() first.");
564
604
  }
565
- // Check if the history provider supports switching sessions
566
- // SQLiteChatMessageHistory does support it via constructor (new instance) or maybe we can add a method there too?
567
- // Actually SQLiteChatMessageHistory has `switchSession(targetSessionId)` but that one logic is "pause current, activate target".
568
- // For API usage, we might just want to *target* a session without necessarily changing the global "active" state regarding the Daemon?
569
- //
570
- // However, the user request implies this is "the" chat.
571
- // If we use `switchSession` it pauses others. That seems correct for a single-user agent model.
572
- //
573
- // But `SQLiteChatMessageHistory` properties are `sessionId`.
574
- // It seems `switchSession` in `sqlite.ts` updates the DB state.
575
- // We also need to update the `sessionId` property of the `SQLiteChatMessageHistory` instance held by Oracle.
576
- //
577
- // Let's check `SQLiteChatMessageHistory` again.
578
- // It has `sessionId` property.
579
- // It does NOT have a method to just update `sessionId` property without DB side effects?
580
- //
581
- // Use `switchSession` from `sqlite.ts` is good for "Active/Paused" state management.
582
- // But we also need the `history` instance to know it is now pointing to `sessionId`.
583
605
  if (this.history instanceof SQLiteChatMessageHistory) {
584
- // Logic:
585
- // 1. If currently active session is different, switch.
586
- // 2. Update internal sessionId.
587
- // Actually `switchSession` in `sqlite.ts` takes `targetSessionId`.
588
- // It updates the DB status.
589
- // It DOES NOT seem to update `this.sessionId` of the instance?
590
- // Wait, let me check `sqlite.ts` content from memory or view it again alongside.
591
- //
592
- // In `sqlite.ts`:
593
- // public async switchSession(targetSessionId: string): Promise<void> { ... }
594
- // It updates DB.
595
- // It DOES NOT update `this.sessionId`.
596
- //
597
- // So we need to ensure `this.history` points to the new session.
598
- // Since `SQLiteChatMessageHistory` might not allow changing `sessionId` publicly if it's protected/private...
599
- // It is `private sessionId: string;`.
600
- //
601
- // So simple fix: Re-instantiate `this.history`?
602
- // `this.history = new SQLiteChatMessageHistory({ sessionId: sessionId, ... })`
603
- //
604
- // This is safe and clean.
605
- // Ensure the target session exists before switching (creates as 'paused' if not found).
606
+ // Ensure the target session exists in DB
606
607
  this.history.ensureSession(sessionId);
607
- await this.history.switchSession(sessionId);
608
608
  // Close previous connection before re-instantiating to avoid file handle leaks
609
609
  this.history.close();
610
610
  // Re-instantiate to point to new session
@@ -645,6 +645,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
645
645
  await Trinity.refreshDelegateCatalog().catch(() => { });
646
646
  updateSkillToolDescriptions();
647
647
  this.provider = await ProviderFactory.create(this.config.llm, [
648
+ buildSetupTool(),
648
649
  TaskQueryTool,
649
650
  Neo.getInstance().createDelegateTool(),
650
651
  Apoc.getInstance().createDelegateTool(),