morpheus-cli 0.9.32 → 0.9.40

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 (62) hide show
  1. package/dist/channels/telegram.js +1 -1
  2. package/dist/cli/commands/start.js +7 -0
  3. package/dist/config/paths.js +1 -0
  4. package/dist/config/schemas.js +7 -0
  5. package/dist/http/api.js +5 -0
  6. package/dist/http/routers/model-presets.js +169 -0
  7. package/dist/http/routers/oauth.js +93 -0
  8. package/dist/http/server.js +4 -0
  9. package/dist/runtime/memory/sati/service.js +13 -3
  10. package/dist/runtime/memory/sqlite.js +162 -20
  11. package/dist/runtime/oauth/manager.js +218 -0
  12. package/dist/runtime/oauth/provider.js +90 -0
  13. package/dist/runtime/oauth/store.js +118 -0
  14. package/dist/runtime/oauth/types.js +1 -0
  15. package/dist/runtime/oracle.js +18 -7
  16. package/dist/runtime/providers/factory.js +93 -1
  17. package/dist/runtime/tasks/event-bus.js +11 -0
  18. package/dist/runtime/tasks/notifier.js +57 -31
  19. package/dist/runtime/tasks/repository.js +21 -0
  20. package/dist/runtime/tasks/worker.js +11 -7
  21. package/dist/runtime/tools/cache.js +66 -47
  22. package/dist/ui/assets/{AuditDashboard-rap15I-4.js → AuditDashboard-EvtKjy5H.js} +1 -1
  23. package/dist/ui/assets/{Chat-CnuRZFBT.js → Chat-yptierPt.js} +3 -3
  24. package/dist/ui/assets/{Chronos-C81_HP6e.js → Chronos-BA77MYbp.js} +1 -1
  25. package/dist/ui/assets/{ConfirmationModal-CT_v8cAi.js → ConfirmationModal-NOZr-ipQ.js} +1 -1
  26. package/dist/ui/assets/{Dashboard-0VfNJ9BZ.js → Dashboard-ly1MJiB4.js} +1 -1
  27. package/dist/ui/assets/{DeleteConfirmationModal-P2foiqon.js → DeleteConfirmationModal-2HMraacH.js} +1 -1
  28. package/dist/ui/assets/{Documents-C8UfbcGD.js → Documents-C31fAm0Z.js} +2 -2
  29. package/dist/ui/assets/{Logs-qdsCW9u9.js → Logs-BiajoLAB.js} +1 -1
  30. package/dist/ui/assets/{MCPManager-CaZLnrKz.js → MCPManager-DS9jfiZT.js} +1 -1
  31. package/dist/ui/assets/ModelPresets-CxhKcalw.js +1 -0
  32. package/dist/ui/assets/{ModelPricing-C73OfGhc.js → ModelPricing-CN8flHnP.js} +1 -1
  33. package/dist/ui/assets/{Notifications-CwqeagwF.js → Notifications-BfP1_CM3.js} +1 -1
  34. package/dist/ui/assets/{Pagination-3P6KG-u6.js → Pagination-Doam4_qd.js} +1 -1
  35. package/dist/ui/assets/SatiMemories-Bk4_ubo7.js +1 -0
  36. package/dist/ui/assets/{SessionAudit-Cykp4Sv_.js → SessionAudit-D3E6QSQw.js} +2 -2
  37. package/dist/ui/assets/Settings-3VBK8muv.js +49 -0
  38. package/dist/ui/assets/{Skills-B6io4GZh.js → Skills-Dp0_GwiW.js} +1 -1
  39. package/dist/ui/assets/{Smiths-XoDzX1K0.js → Smiths-COTgI2R4.js} +1 -1
  40. package/dist/ui/assets/{Tasks-vui0C_76.js → Tasks-COe4lIJ7.js} +1 -1
  41. package/dist/ui/assets/{TrinityDatabases-Dp71dyUn.js → TrinityDatabases-BEU4mmyW.js} +1 -1
  42. package/dist/ui/assets/{UsageStats-Dz4LXfr4.js → UsageStats-BTmDeG2V.js} +1 -1
  43. package/dist/ui/assets/{WebhookManager-CC4Mbo2v.js → WebhookManager-FQVyKyW-.js} +2 -2
  44. package/dist/ui/assets/{agents-DV1oGA7P.js → agents-B6e9N0QI.js} +1 -1
  45. package/dist/ui/assets/{audit-DnegNntR.js → audit-giQG2WRk.js} +1 -1
  46. package/dist/ui/assets/{chronos-BDlP8kzg.js → chronos-sweaRcNj.js} +1 -1
  47. package/dist/ui/assets/{config-BhjCL4aM.js → config-CbUdj76n.js} +1 -1
  48. package/dist/ui/assets/index-CRPT77Yo.css +1 -0
  49. package/dist/ui/assets/{index-C3qfojVn.js → index-yu2c4ry1.js} +7 -7
  50. package/dist/ui/assets/{mcp-uYhIyjyx.js → mcp-v64BBpUk.js} +1 -1
  51. package/dist/ui/assets/modelPresets-BaNh-gxn.js +1 -0
  52. package/dist/ui/assets/{skills-_9hplz7d.js → skills-ClRXBlVt.js} +1 -1
  53. package/dist/ui/assets/{stats-BwaWi9yN.js → stats-nI-89hEX.js} +1 -1
  54. package/dist/ui/assets/{useCurrency-RBarItCC.js → useCurrency-D5An8I2f.js} +1 -1
  55. package/dist/ui/assets/vendor-icons-LSkmAkBj.js +1 -0
  56. package/dist/ui/index.html +3 -3
  57. package/dist/ui/sw.js +1 -1
  58. package/package.json +1 -1
  59. package/dist/ui/assets/SatiMemories-CVhOdyAk.js +0 -1
  60. package/dist/ui/assets/Settings-DnyG6tDx.js +0 -49
  61. package/dist/ui/assets/index-gx__iEcl.css +0 -1
  62. package/dist/ui/assets/vendor-icons-tocJCdt5.js +0 -1
@@ -0,0 +1,218 @@
1
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { DynamicStructuredTool } from '@langchain/core/tools';
4
+ import { OAuthStore } from './store.js';
5
+ import { MorpheusOAuthProvider } from './provider.js';
6
+ import { DisplayManager } from '../display.js';
7
+ const display = DisplayManager.getInstance();
8
+ /**
9
+ * OAuthManager — singleton coordinating OAuth 2 flows for HTTP MCP servers.
10
+ *
11
+ * Responsibilities:
12
+ * - Creates MorpheusOAuthProvider per server
13
+ * - Tracks pending transports awaiting user authorization (auth_code flow)
14
+ * - Handles callback (finishAuth) after user authorizes
15
+ * - Provides status of all OAuth MCP servers
16
+ */
17
+ export class OAuthManager {
18
+ static instance = null;
19
+ store;
20
+ pending = new Map();
21
+ redirectUri;
22
+ notifyFn = null;
23
+ constructor(port) {
24
+ this.store = OAuthStore.getInstance();
25
+ this.redirectUri = `http://localhost:${port}/api/oauth/callback`;
26
+ }
27
+ static getInstance(port) {
28
+ if (!OAuthManager.instance) {
29
+ if (!port)
30
+ throw new Error('OAuthManager.getInstance() requires port on first call');
31
+ OAuthManager.instance = new OAuthManager(port);
32
+ }
33
+ return OAuthManager.instance;
34
+ }
35
+ static resetInstance() {
36
+ OAuthManager.instance = null;
37
+ }
38
+ /** Set the notification function (called when user needs to authorize) */
39
+ setNotifyFn(fn) {
40
+ this.notifyFn = fn;
41
+ }
42
+ getRedirectUri() {
43
+ return this.redirectUri;
44
+ }
45
+ // ── Connection ────────────────────────────────────────
46
+ /**
47
+ * Attempt to connect to an HTTP MCP server with OAuth support.
48
+ * Returns loaded tools if successful, or empty array if auth is pending.
49
+ */
50
+ async connectHttpMcp(serverName, serverUrl, oauth2Config, extraHeaders) {
51
+ const provider = new MorpheusOAuthProvider(serverName, this.store, async (url) => {
52
+ display.log(`MCP '${serverName}' requires OAuth authorization`, {
53
+ level: 'info',
54
+ source: 'OAuth',
55
+ meta: { url: url.toString() },
56
+ });
57
+ if (this.notifyFn) {
58
+ await this.notifyFn(serverName, url);
59
+ }
60
+ }, this.redirectUri, oauth2Config?.scope, oauth2Config?.client_id, oauth2Config?.client_secret, oauth2Config?.grant_type);
61
+ const transportUrl = new URL(serverUrl);
62
+ const transport = new StreamableHTTPClientTransport(transportUrl, {
63
+ authProvider: provider,
64
+ requestInit: extraHeaders ? { headers: extraHeaders } : undefined,
65
+ });
66
+ try {
67
+ const client = new Client({ name: `morpheus-${serverName}`, version: '1.0.0' });
68
+ await client.connect(transport);
69
+ // Connection succeeded — extract tools
70
+ const { tools: toolDefs } = await client.listTools();
71
+ const tools = toolDefs.map((td) => this.mcpToolToStructuredTool(serverName, td, client));
72
+ display.log(`OAuth MCP '${serverName}': loaded ${tools.length} tools`, {
73
+ level: 'info',
74
+ source: 'OAuth',
75
+ });
76
+ return { tools, authPending: false };
77
+ }
78
+ catch (error) {
79
+ // Check if auth redirect was triggered (SDK calls redirectToAuthorization)
80
+ if (error?.message?.includes('Unauthorized') || error?.code === 401) {
81
+ display.log(`OAuth MCP '${serverName}': authorization required, waiting for user`, {
82
+ level: 'info',
83
+ source: 'OAuth',
84
+ });
85
+ this.pending.set(serverName, { transport, provider, serverUrl });
86
+ return { tools: [], authPending: true };
87
+ }
88
+ // The SDK may throw after calling redirectToAuthorization for auth_code flow.
89
+ // Check if we have a pending state for this server (set during redirectToAuthorization).
90
+ const hasPkce = this.store.getLatestPkceVerifier(serverName);
91
+ if (hasPkce) {
92
+ display.log(`OAuth MCP '${serverName}': authorization redirect sent, waiting for callback`, {
93
+ level: 'info',
94
+ source: 'OAuth',
95
+ });
96
+ this.pending.set(serverName, { transport, provider, serverUrl });
97
+ return { tools: [], authPending: true };
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ // ── Callback ──────────────────────────────────────────
103
+ /**
104
+ * Handle OAuth callback after user authorizes.
105
+ * Called by GET /api/oauth/callback?code=...&state=...
106
+ */
107
+ async finishAuth(code, state) {
108
+ // Resolve which server this callback is for
109
+ let serverName;
110
+ if (state) {
111
+ serverName = this.store.resolveServerByState(state);
112
+ }
113
+ if (!serverName) {
114
+ // Fallback: use the only pending server if there's just one
115
+ const pendingNames = [...this.pending.keys()];
116
+ if (pendingNames.length === 1) {
117
+ serverName = pendingNames[0];
118
+ }
119
+ }
120
+ if (!serverName) {
121
+ throw new Error('Could not resolve OAuth callback: unknown state parameter');
122
+ }
123
+ const entry = this.pending.get(serverName);
124
+ if (!entry) {
125
+ throw new Error(`No pending OAuth flow for server '${serverName}'`);
126
+ }
127
+ // Complete the auth flow — SDK exchanges code for token
128
+ await entry.transport.finishAuth(code);
129
+ this.pending.delete(serverName);
130
+ // Clean up PKCE
131
+ if (state)
132
+ this.store.deletePkce(serverName, state);
133
+ // Now reconnect to get tools
134
+ const client = new Client({ name: `morpheus-${serverName}`, version: '1.0.0' });
135
+ const transport = new StreamableHTTPClientTransport(new URL(entry.serverUrl), {
136
+ authProvider: entry.provider,
137
+ });
138
+ await client.connect(transport);
139
+ const { tools: toolDefs } = await client.listTools();
140
+ display.log(`OAuth MCP '${serverName}': authorized! ${toolDefs.length} tools available`, {
141
+ level: 'info',
142
+ source: 'OAuth',
143
+ });
144
+ return { serverName, toolCount: toolDefs.length };
145
+ }
146
+ // ── Status ────────────────────────────────────────────
147
+ getStatus() {
148
+ const statuses = [];
149
+ for (const name of this.store.listServers()) {
150
+ const tokens = this.store.getTokens(name);
151
+ if (!tokens) {
152
+ statuses.push({ name, status: this.pending.has(name) ? 'pending_auth' : 'no_token' });
153
+ continue;
154
+ }
155
+ // OAuthTokens may have expires_in but not expires_at — check if present
156
+ const expiresAt = tokens.expires_at;
157
+ if (expiresAt && Date.now() > expiresAt) {
158
+ statuses.push({ name, status: 'expired', expiresAt });
159
+ }
160
+ else {
161
+ statuses.push({ name, status: 'authorized', expiresAt });
162
+ }
163
+ }
164
+ // Add pending servers that aren't in the store yet
165
+ for (const name of this.pending.keys()) {
166
+ if (!statuses.find(s => s.name === name)) {
167
+ statuses.push({ name, status: 'pending_auth' });
168
+ }
169
+ }
170
+ return statuses;
171
+ }
172
+ isPending(serverName) {
173
+ return this.pending.has(serverName);
174
+ }
175
+ // ── Revoke ────────────────────────────────────────────
176
+ revokeToken(serverName) {
177
+ this.store.deleteTokens(serverName);
178
+ this.pending.delete(serverName);
179
+ display.log(`OAuth tokens revoked for MCP '${serverName}'`, {
180
+ level: 'info',
181
+ source: 'OAuth',
182
+ });
183
+ }
184
+ // ── Internal: convert MCP tool def to LangChain StructuredTool ──
185
+ mcpToolToStructuredTool(serverName, toolDef, mcpClient) {
186
+ const prefixedName = `${serverName}_${toolDef.name}`;
187
+ const originalToolName = toolDef.name;
188
+ // Pass raw JSON Schema directly (same approach as @langchain/mcp-adapters).
189
+ // Avoids Zod v4 $required serialization issues with LangChain's zodToJsonSchema.
190
+ const rawSchema = toolDef.inputSchema ?? { type: 'object', properties: {} };
191
+ if (!rawSchema.properties)
192
+ rawSchema.properties = {};
193
+ return new DynamicStructuredTool({
194
+ name: prefixedName,
195
+ description: toolDef.description || `MCP tool ${toolDef.name} from ${serverName}`,
196
+ schema: rawSchema,
197
+ func: async (input) => {
198
+ try {
199
+ const result = await mcpClient.callTool({
200
+ name: originalToolName,
201
+ arguments: input,
202
+ });
203
+ if (Array.isArray(result.content)) {
204
+ return result.content
205
+ .map((c) => c.text ?? JSON.stringify(c))
206
+ .join('\n');
207
+ }
208
+ return typeof result.content === 'string'
209
+ ? result.content
210
+ : JSON.stringify(result.content);
211
+ }
212
+ catch (error) {
213
+ return `Error calling MCP tool ${originalToolName}: ${error.message}`;
214
+ }
215
+ },
216
+ });
217
+ }
218
+ }
@@ -0,0 +1,90 @@
1
+ import crypto from 'crypto';
2
+ /**
3
+ * Morpheus implementation of OAuthClientProvider from the MCP SDK.
4
+ * Delegates persistence to OAuthStore and user notification to a callback.
5
+ */
6
+ export class MorpheusOAuthProvider {
7
+ serverName;
8
+ store;
9
+ notifyUser;
10
+ _redirectUri;
11
+ _scope;
12
+ _clientId;
13
+ _clientSecret;
14
+ _grantType;
15
+ _lastState;
16
+ constructor(serverName, store, notifyUser, _redirectUri, _scope, _clientId, _clientSecret, _grantType) {
17
+ this.serverName = serverName;
18
+ this.store = store;
19
+ this.notifyUser = notifyUser;
20
+ this._redirectUri = _redirectUri;
21
+ this._scope = _scope;
22
+ this._clientId = _clientId;
23
+ this._clientSecret = _clientSecret;
24
+ this._grantType = _grantType;
25
+ }
26
+ get redirectUrl() {
27
+ if (this._grantType === 'client_credentials')
28
+ return undefined;
29
+ return this._redirectUri;
30
+ }
31
+ get clientMetadata() {
32
+ const meta = {
33
+ redirect_uris: [new URL(this._redirectUri)],
34
+ client_name: `Morpheus - ${this.serverName}`,
35
+ grant_types: this._grantType === 'client_credentials'
36
+ ? ['client_credentials']
37
+ : ['authorization_code', 'refresh_token'],
38
+ response_types: this._grantType === 'client_credentials' ? [] : ['code'],
39
+ };
40
+ if (this._scope)
41
+ meta.scope = this._scope;
42
+ return meta;
43
+ }
44
+ async state() {
45
+ this._lastState = crypto.randomUUID();
46
+ return this._lastState;
47
+ }
48
+ async clientInformation() {
49
+ // If user provided client_id, return it as pre-registered info
50
+ if (this._clientId) {
51
+ return {
52
+ client_id: this._clientId,
53
+ client_secret: this._clientSecret,
54
+ };
55
+ }
56
+ // Otherwise check store for dynamically registered client info
57
+ return this.store.getClientInfo(this.serverName);
58
+ }
59
+ async saveClientInformation(info) {
60
+ // OAuthClientInformationFull has client_id_issued_at
61
+ this.store.saveClientInfo(this.serverName, info);
62
+ }
63
+ async tokens() {
64
+ return this.store.getTokens(this.serverName);
65
+ }
66
+ async saveTokens(tokens) {
67
+ this.store.saveTokens(this.serverName, tokens);
68
+ }
69
+ async redirectToAuthorization(authorizationUrl) {
70
+ await this.notifyUser(authorizationUrl);
71
+ }
72
+ async saveCodeVerifier(codeVerifier) {
73
+ const state = this._lastState ?? '__default';
74
+ this.store.savePkceVerifier(this.serverName, state, codeVerifier);
75
+ }
76
+ async codeVerifier() {
77
+ const verifier = this.store.getLatestPkceVerifier(this.serverName);
78
+ if (!verifier)
79
+ throw new Error(`No PKCE code verifier found for ${this.serverName}`);
80
+ return verifier;
81
+ }
82
+ async invalidateCredentials(scope) {
83
+ if (scope === 'all' || scope === 'tokens') {
84
+ this.store.deleteTokens(this.serverName);
85
+ }
86
+ if (scope === 'all') {
87
+ this.store.deleteServer(this.serverName);
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PATHS } from '../../config/paths.js';
4
+ /**
5
+ * Persists OAuth tokens, client info, and PKCE state in ~/.morpheus/oauth-tokens.json.
6
+ * Atomic writes (temp → rename). Lazy reads.
7
+ */
8
+ export class OAuthStore {
9
+ static instance = null;
10
+ data = null;
11
+ filePath;
12
+ constructor() {
13
+ this.filePath = PATHS.oauthTokens;
14
+ }
15
+ static getInstance() {
16
+ if (!OAuthStore.instance) {
17
+ OAuthStore.instance = new OAuthStore();
18
+ }
19
+ return OAuthStore.instance;
20
+ }
21
+ static resetInstance() {
22
+ OAuthStore.instance = null;
23
+ }
24
+ // ── Read ──────────────────────────────────────────────
25
+ read() {
26
+ if (this.data)
27
+ return this.data;
28
+ try {
29
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
30
+ this.data = JSON.parse(raw);
31
+ }
32
+ catch {
33
+ this.data = {};
34
+ }
35
+ return this.data;
36
+ }
37
+ getRecord(serverName) {
38
+ const data = this.read();
39
+ if (!data[serverName])
40
+ data[serverName] = {};
41
+ return data[serverName];
42
+ }
43
+ // ── Write ─────────────────────────────────────────────
44
+ persist() {
45
+ const dir = path.dirname(this.filePath);
46
+ if (!fs.existsSync(dir))
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ const tmp = this.filePath + '.tmp';
49
+ fs.writeFileSync(tmp, JSON.stringify(this.data ?? {}, null, 2), 'utf-8');
50
+ fs.renameSync(tmp, this.filePath);
51
+ }
52
+ // ── Tokens ────────────────────────────────────────────
53
+ getTokens(serverName) {
54
+ return this.getRecord(serverName).tokens;
55
+ }
56
+ saveTokens(serverName, tokens) {
57
+ this.getRecord(serverName).tokens = tokens;
58
+ this.persist();
59
+ }
60
+ deleteTokens(serverName) {
61
+ const record = this.getRecord(serverName);
62
+ delete record.tokens;
63
+ this.persist();
64
+ }
65
+ // ── Client Info (dynamic registration) ────────────────
66
+ getClientInfo(serverName) {
67
+ return this.getRecord(serverName).clientInfo;
68
+ }
69
+ saveClientInfo(serverName, info) {
70
+ this.getRecord(serverName).clientInfo = info;
71
+ this.persist();
72
+ }
73
+ // ── PKCE ──────────────────────────────────────────────
74
+ savePkceVerifier(serverName, state, verifier) {
75
+ const record = this.getRecord(serverName);
76
+ if (!record.pkce)
77
+ record.pkce = {};
78
+ record.pkce[state] = verifier;
79
+ this.persist();
80
+ }
81
+ getPkceVerifier(serverName, state) {
82
+ return this.getRecord(serverName).pkce?.[state];
83
+ }
84
+ getLatestPkceVerifier(serverName) {
85
+ const pkce = this.getRecord(serverName).pkce;
86
+ if (!pkce)
87
+ return undefined;
88
+ const entries = Object.values(pkce);
89
+ return entries[entries.length - 1];
90
+ }
91
+ deletePkce(serverName, state) {
92
+ const pkce = this.getRecord(serverName).pkce;
93
+ if (pkce) {
94
+ delete pkce[state];
95
+ this.persist();
96
+ }
97
+ }
98
+ // ── Queries ───────────────────────────────────────────
99
+ /** List all server names with stored OAuth data */
100
+ listServers() {
101
+ return Object.keys(this.read());
102
+ }
103
+ /** Remove all data for a server */
104
+ deleteServer(serverName) {
105
+ const data = this.read();
106
+ delete data[serverName];
107
+ this.persist();
108
+ }
109
+ /** Map state → serverName for callback resolution */
110
+ resolveServerByState(state) {
111
+ const data = this.read();
112
+ for (const [name, record] of Object.entries(data)) {
113
+ if (record.pkce?.[state])
114
+ return name;
115
+ }
116
+ return undefined;
117
+ }
118
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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 { writeFileSync } from "fs";
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
- try {
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
- memoryMessage = await this.satiMiddleware.beforeAgent(message, previousMessages, currentSessionId);
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
@@ -5,8 +5,99 @@ import { ConfigManager } from "../../config/manager.js";
5
5
  import { TaskRequestContext } from "../tasks/context.js";
6
6
  import { ChannelRegistry } from "../../channels/registry.js";
7
7
  import { getStrategy, registerStrategy } from "./strategies.js";
8
+ import { isInteropZodSchema } from "@langchain/core/utils/types";
9
+ import { toJsonSchema } from "@langchain/core/utils/json_schema";
8
10
  /** Channels that should NOT receive verbose tool notifications */
9
11
  const SILENT_CHANNELS = new Set(['api', 'ui']);
12
+ /** The ONLY JSON Schema keywords the Gemini API accepts.
13
+ * Anything not in this set is stripped after $ref dereferencing. */
14
+ const GEMINI_ALLOWED_SCHEMA_FIELDS = new Set([
15
+ 'type', 'description', 'properties', 'required',
16
+ 'enum', 'items', 'format', 'nullable',
17
+ 'anyOf', 'oneOf', 'allOf',
18
+ 'minimum', 'maximum',
19
+ ]);
20
+ /**
21
+ * Collect all entries from $defs and definitions into a flat lookup map.
22
+ */
23
+ function collectDefs(obj, defs = {}) {
24
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
25
+ return defs;
26
+ const record = obj;
27
+ for (const key of ['$defs', 'definitions']) {
28
+ if (record[key] && typeof record[key] === 'object' && !Array.isArray(record[key])) {
29
+ Object.assign(defs, record[key]);
30
+ }
31
+ }
32
+ for (const v of Object.values(record))
33
+ collectDefs(v, defs);
34
+ return defs;
35
+ }
36
+ /**
37
+ * Resolve a $ref string like "#/$defs/Foo" or "#/definitions/Foo" to its def name.
38
+ */
39
+ function resolveRef(ref) {
40
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
41
+ return m ? m[1] : null;
42
+ }
43
+ /**
44
+ * Recursively strip unsupported keywords and inline $ref references.
45
+ */
46
+ function sanitizeSchemaForGemini(obj, defs, depth = 0) {
47
+ if (depth > 20)
48
+ return {}; // guard against circular refs
49
+ if (obj === null || typeof obj !== 'object')
50
+ return obj;
51
+ if (Array.isArray(obj))
52
+ return obj.map((v) => sanitizeSchemaForGemini(v, defs, depth));
53
+ const record = obj;
54
+ // Inline $ref — look up and recurse into the definition
55
+ if ('$ref' in record && typeof record['$ref'] === 'string') {
56
+ const defName = resolveRef(record['$ref']);
57
+ if (defName && defs[defName]) {
58
+ return sanitizeSchemaForGemini(defs[defName], defs, depth + 1);
59
+ }
60
+ return {}; // unresolvable ref — fall back to empty schema
61
+ }
62
+ const out = {};
63
+ for (const [k, v] of Object.entries(record)) {
64
+ if (!GEMINI_ALLOWED_SCHEMA_FIELDS.has(k))
65
+ continue;
66
+ out[k] = sanitizeSchemaForGemini(v, defs, depth);
67
+ }
68
+ // Ensure every entry in `required` has a matching key in `properties`
69
+ if (Array.isArray(out['required']) && out['properties'] && typeof out['properties'] === 'object') {
70
+ const props = out['properties'];
71
+ out['required'] = out['required'].filter((r) => r in props);
72
+ if (out['required'].length === 0)
73
+ delete out['required'];
74
+ }
75
+ return out;
76
+ }
77
+ function sanitizeToolSchemasForGemini(tools) {
78
+ for (const tool of tools) {
79
+ const schema = tool.schema;
80
+ if (!schema || typeof schema !== 'object')
81
+ continue;
82
+ // If the schema is a Zod instance, convert to plain JSON Schema first.
83
+ // Otherwise LangChain recognises it as Zod and re-generates $defs.
84
+ let jsonSchema;
85
+ if (isInteropZodSchema(schema)) {
86
+ try {
87
+ jsonSchema = toJsonSchema(schema);
88
+ }
89
+ catch {
90
+ jsonSchema = schema;
91
+ }
92
+ }
93
+ else {
94
+ jsonSchema = schema;
95
+ }
96
+ const defs = collectDefs(jsonSchema);
97
+ tool.schema = sanitizeSchemaForGemini(jsonSchema, defs);
98
+ }
99
+ return tools;
100
+ }
10
101
  export { registerStrategy };
11
102
  export class ProviderFactory {
12
103
  static buildMonitoringMiddleware() {
@@ -84,7 +175,8 @@ export class ProviderFactory {
84
175
  try {
85
176
  const model = ProviderFactory.buildModel(config);
86
177
  const middleware = ProviderFactory.buildMonitoringMiddleware();
87
- return createAgent({ model, tools, middleware: [middleware] });
178
+ const safeTools = config.provider === 'gemini' ? sanitizeToolSchemasForGemini(tools) : tools;
179
+ return createAgent({ model, tools: safeTools, middleware: [middleware] });
88
180
  }
89
181
  catch (error) {
90
182
  ProviderFactory.handleProviderError(config, error);
@@ -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();