@hailer/mcp 0.1.15 → 0.1.17
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/.claude/agents/agent-giuseppe-app-builder.md +7 -6
- package/.claude/agents/agent-lars-code-inspector.md +26 -14
- package/dist/agents/bot-manager.d.ts +48 -0
- package/dist/agents/bot-manager.js +254 -0
- package/dist/agents/factory.d.ts +150 -0
- package/dist/agents/factory.js +650 -0
- package/dist/agents/giuseppe/ai.d.ts +83 -0
- package/dist/agents/giuseppe/ai.js +466 -0
- package/dist/agents/giuseppe/bot.d.ts +110 -0
- package/dist/agents/giuseppe/bot.js +780 -0
- package/dist/agents/giuseppe/config.d.ts +25 -0
- package/dist/agents/giuseppe/config.js +227 -0
- package/dist/agents/giuseppe/files.d.ts +52 -0
- package/dist/agents/giuseppe/files.js +338 -0
- package/dist/agents/giuseppe/git.d.ts +48 -0
- package/dist/agents/giuseppe/git.js +298 -0
- package/dist/agents/giuseppe/index.d.ts +97 -0
- package/dist/agents/giuseppe/index.js +258 -0
- package/dist/agents/giuseppe/lsp.d.ts +113 -0
- package/dist/agents/giuseppe/lsp.js +485 -0
- package/dist/agents/giuseppe/monitor.d.ts +118 -0
- package/dist/agents/giuseppe/monitor.js +621 -0
- package/dist/agents/giuseppe/prompt.d.ts +5 -0
- package/dist/agents/giuseppe/prompt.js +94 -0
- package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
- package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
- package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
- package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
- package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
- package/dist/agents/giuseppe/registries/pending.js +49 -0
- package/dist/agents/giuseppe/specialist.d.ts +47 -0
- package/dist/agents/giuseppe/specialist.js +237 -0
- package/dist/agents/giuseppe/types.d.ts +123 -0
- package/dist/agents/giuseppe/types.js +9 -0
- package/dist/agents/hailer-expert/index.d.ts +8 -0
- package/dist/agents/hailer-expert/index.js +14 -0
- package/dist/agents/hal/daemon.d.ts +142 -0
- package/dist/agents/hal/daemon.js +1103 -0
- package/dist/agents/hal/definitions.d.ts +55 -0
- package/dist/agents/hal/definitions.js +263 -0
- package/dist/agents/hal/index.d.ts +3 -0
- package/dist/agents/hal/index.js +8 -0
- package/dist/agents/index.d.ts +18 -0
- package/dist/agents/index.js +48 -0
- package/dist/agents/shared/base.d.ts +216 -0
- package/dist/agents/shared/base.js +846 -0
- package/dist/agents/shared/services/agent-registry.d.ts +107 -0
- package/dist/agents/shared/services/agent-registry.js +629 -0
- package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
- package/dist/agents/shared/services/conversation-manager.js +136 -0
- package/dist/agents/shared/services/mcp-client.d.ts +56 -0
- package/dist/agents/shared/services/mcp-client.js +124 -0
- package/dist/agents/shared/services/message-classifier.d.ts +37 -0
- package/dist/agents/shared/services/message-classifier.js +187 -0
- package/dist/agents/shared/services/message-formatter.d.ts +89 -0
- package/dist/agents/shared/services/message-formatter.js +371 -0
- package/dist/agents/shared/services/session-logger.d.ts +106 -0
- package/dist/agents/shared/services/session-logger.js +446 -0
- package/dist/agents/shared/services/tool-executor.d.ts +41 -0
- package/dist/agents/shared/services/tool-executor.js +169 -0
- package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
- package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
- package/dist/agents/shared/specialist.d.ts +91 -0
- package/dist/agents/shared/specialist.js +399 -0
- package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
- package/dist/agents/shared/tool-schema-loader.js +232 -0
- package/dist/agents/shared/types.d.ts +327 -0
- package/dist/agents/shared/types.js +121 -0
- package/dist/app.js +21 -4
- package/dist/cli.js +0 -0
- package/dist/client/agents/orchestrator.d.ts +1 -0
- package/dist/client/agents/orchestrator.js +12 -1
- package/dist/commands/seed-config.d.ts +9 -0
- package/dist/commands/seed-config.js +372 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +61 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.js +137 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.js +2 -2
- package/dist/mcp/hailer-clients.d.ts +15 -0
- package/dist/mcp/hailer-clients.js +100 -6
- package/dist/mcp/signal-handler.d.ts +16 -5
- package/dist/mcp/signal-handler.js +173 -122
- package/dist/mcp/tools/activity.js +9 -1
- package/dist/mcp/tools/bot-config.d.ts +184 -9
- package/dist/mcp/tools/bot-config.js +2177 -163
- package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
- package/dist/mcp/tools/giuseppe-tools.js +525 -0
- package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
- package/dist/mcp/utils/hailer-api-client.js +128 -2
- package/dist/mcp/webhook-handler.d.ts +87 -0
- package/dist/mcp/webhook-handler.js +345 -0
- package/dist/mcp/workspace-cache.d.ts +5 -0
- package/dist/mcp/workspace-cache.js +11 -0
- package/dist/mcp-server.js +60 -5
- package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
- package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
- package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
- package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
- package/dist/modules/bug-reports/giuseppe-bot.d.ts +2 -2
- package/dist/modules/bug-reports/giuseppe-bot.js +66 -42
- package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
- package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
- package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
- package/dist/modules/bug-reports/giuseppe-files.js +37 -0
- package/dist/modules/bug-reports/giuseppe-lsp.d.ts +84 -13
- package/dist/modules/bug-reports/giuseppe-lsp.js +403 -61
- package/dist/modules/bug-reports/index.d.ts +1 -0
- package/dist/modules/bug-reports/index.js +31 -29
- package/package.json +3 -2
package/dist/core.js
CHANGED
|
@@ -8,12 +8,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.Core = void 0;
|
|
9
9
|
const logger_1 = require("./lib/logger");
|
|
10
10
|
const config_1 = require("./config");
|
|
11
|
-
const factory_1 = require("./
|
|
11
|
+
const factory_1 = require("./agents/factory");
|
|
12
12
|
const mcp_server_1 = require("./mcp-server");
|
|
13
13
|
const tool_registry_1 = require("./mcp/tool-registry");
|
|
14
|
-
const
|
|
14
|
+
const giuseppe_1 = require("./agents/giuseppe");
|
|
15
15
|
const UserContextCache_1 = require("./mcp/UserContextCache");
|
|
16
16
|
const bot_config_1 = require("./mcp/tools/bot-config");
|
|
17
|
+
const webhook_handler_1 = require("./mcp/webhook-handler");
|
|
17
18
|
class Core {
|
|
18
19
|
logger;
|
|
19
20
|
appConfig;
|
|
@@ -22,6 +23,8 @@ class Core {
|
|
|
22
23
|
daemonManager = null;
|
|
23
24
|
statusLogInterval;
|
|
24
25
|
bugReportsModule;
|
|
26
|
+
daemonInitInProgress = false;
|
|
27
|
+
initialDaemonReady = false; // Prevents restart attempts before first daemon init
|
|
25
28
|
constructor() {
|
|
26
29
|
// Initialize logger first
|
|
27
30
|
this.logger = (0, logger_1.createLogger)({
|
|
@@ -91,9 +94,15 @@ class Core {
|
|
|
91
94
|
this.logger.info('MCP Server service started');
|
|
92
95
|
}
|
|
93
96
|
async startMCPClient() {
|
|
97
|
+
if (this.daemonInitInProgress) {
|
|
98
|
+
this.logger.debug('Daemon init already in progress, skipping startMCPClient');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
94
101
|
this.logger.info('Starting Chat Agent Daemon (persistent conversation mode)');
|
|
102
|
+
this.daemonInitInProgress = true;
|
|
95
103
|
try {
|
|
96
104
|
await this.initializeDaemonMode();
|
|
105
|
+
this.initialDaemonReady = true; // Mark daemon as ready for restart signals
|
|
97
106
|
this.logger.info('Chat Agent Daemon started');
|
|
98
107
|
}
|
|
99
108
|
catch (error) {
|
|
@@ -101,6 +110,9 @@ class Core {
|
|
|
101
110
|
error: error instanceof Error ? error.message : String(error)
|
|
102
111
|
});
|
|
103
112
|
}
|
|
113
|
+
finally {
|
|
114
|
+
this.daemonInitInProgress = false;
|
|
115
|
+
}
|
|
104
116
|
}
|
|
105
117
|
async startBugMonitor() {
|
|
106
118
|
try {
|
|
@@ -112,7 +124,7 @@ class Core {
|
|
|
112
124
|
}
|
|
113
125
|
const [apiKey] = accounts[0];
|
|
114
126
|
const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
|
|
115
|
-
this.bugReportsModule = new
|
|
127
|
+
this.bugReportsModule = new giuseppe_1.BugReportsModule(userContext);
|
|
116
128
|
// Register bot user IDs to ignore (so bug monitor only processes human messages)
|
|
117
129
|
if (this.daemonManager) {
|
|
118
130
|
const daemonStatus = this.daemonManager.getStatus();
|
|
@@ -155,6 +167,10 @@ class Core {
|
|
|
155
167
|
const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
|
|
156
168
|
await (0, bot_config_1.initBotConfigPersistence)(userContext.hailer);
|
|
157
169
|
this.logger.info('Bot config persistence initialized');
|
|
170
|
+
// Register callback for daemon restart when new orchestrator is discovered
|
|
171
|
+
(0, bot_config_1.onDaemonRestartNeeded)(() => {
|
|
172
|
+
this.attemptDaemonRestart();
|
|
173
|
+
});
|
|
158
174
|
}
|
|
159
175
|
catch (error) {
|
|
160
176
|
this.logger.warn('Bot config persistence failed to initialize - using in-memory defaults', {
|
|
@@ -162,12 +178,83 @@ class Core {
|
|
|
162
178
|
});
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Attempt to restart daemon mode when bot state changes
|
|
183
|
+
* Stops existing daemons and starts fresh with updated configuration
|
|
184
|
+
* Includes retry logic for transient connection failures
|
|
185
|
+
*/
|
|
186
|
+
async attemptDaemonRestart(retryCount = 0, workspaceId) {
|
|
187
|
+
const MAX_RETRIES = 3;
|
|
188
|
+
const RETRY_DELAY_MS = 5000;
|
|
189
|
+
// Skip restart if initial daemon hasn't been created yet
|
|
190
|
+
// (signals can fire before first daemon init completes)
|
|
191
|
+
if (!this.initialDaemonReady) {
|
|
192
|
+
this.logger.debug('Skipping daemon restart - initial daemon not yet ready');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.daemonInitInProgress) {
|
|
196
|
+
this.logger.debug('Daemon init already in progress, skipping');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.logger.info('Restarting daemons after bot state change', { retryCount });
|
|
200
|
+
this.daemonInitInProgress = true;
|
|
201
|
+
try {
|
|
202
|
+
// Stop existing daemons if running
|
|
203
|
+
if (this.daemonManager) {
|
|
204
|
+
this.logger.info('Stopping existing daemons for restart');
|
|
205
|
+
if (this.statusLogInterval) {
|
|
206
|
+
clearInterval(this.statusLogInterval);
|
|
207
|
+
this.statusLogInterval = undefined;
|
|
208
|
+
}
|
|
209
|
+
await this.daemonManager.stopAll();
|
|
210
|
+
this.daemonManager = null;
|
|
211
|
+
}
|
|
212
|
+
// Start fresh with updated bot state
|
|
213
|
+
await this.initializeDaemonMode(workspaceId);
|
|
214
|
+
// Verify orchestrator was created successfully (daemonManager is set by initializeDaemonMode)
|
|
215
|
+
// Use explicit re-read to help TypeScript understand the state change
|
|
216
|
+
const newManager = this.daemonManager;
|
|
217
|
+
if (!newManager) {
|
|
218
|
+
throw new Error('Daemon manager was not created');
|
|
219
|
+
}
|
|
220
|
+
if (!newManager.getOrchestrator()) {
|
|
221
|
+
throw new Error('Daemon manager created but no orchestrator available');
|
|
222
|
+
}
|
|
223
|
+
this.logger.info('Daemons successfully restarted after state change');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
227
|
+
const isTransientError = errorMessage.includes('Timeout') ||
|
|
228
|
+
errorMessage.includes('ECONNREFUSED') ||
|
|
229
|
+
errorMessage.includes('No orchestrator');
|
|
230
|
+
if (isTransientError && retryCount < MAX_RETRIES) {
|
|
231
|
+
this.logger.warn('Daemon restart failed with transient error, scheduling retry', {
|
|
232
|
+
error: errorMessage,
|
|
233
|
+
retryCount: retryCount + 1,
|
|
234
|
+
retryDelayMs: RETRY_DELAY_MS
|
|
235
|
+
});
|
|
236
|
+
// Reset flag before scheduling retry
|
|
237
|
+
this.daemonInitInProgress = false;
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
this.attemptDaemonRestart(retryCount + 1, workspaceId);
|
|
240
|
+
}, RETRY_DELAY_MS);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.logger.warn('Daemon restart failed', {
|
|
244
|
+
error: errorMessage,
|
|
245
|
+
retriesExhausted: retryCount >= MAX_RETRIES
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
this.daemonInitInProgress = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async initializeDaemonMode(workspaceId) {
|
|
253
|
+
this.logger.info('Initializing Chat Agent Daemon', { workspaceId });
|
|
167
254
|
// Check for orchestrator mode via environment variable
|
|
168
255
|
const orchestratorMode = process.env.DAEMON_ORCHESTRATOR_MODE === 'true';
|
|
169
256
|
this.logger.info(`Daemon mode: ${orchestratorMode ? 'ORCHESTRATOR' : 'STANDARD'}`);
|
|
170
|
-
this.daemonManager = await (0, factory_1.createDaemonManager)({ orchestratorMode });
|
|
257
|
+
this.daemonManager = await (0, factory_1.createDaemonManager)({ orchestratorMode, workspaceId });
|
|
171
258
|
if (!this.daemonManager) {
|
|
172
259
|
throw new Error('Failed to create daemon manager');
|
|
173
260
|
}
|
|
@@ -177,6 +264,48 @@ class Core {
|
|
|
177
264
|
bots: status.map(s => s.botId),
|
|
178
265
|
orchestratorMode
|
|
179
266
|
});
|
|
267
|
+
// Wire webhook updates to daemon hot-reload
|
|
268
|
+
(0, webhook_handler_1.onBotUpdate)((workspaceId, bot, action) => {
|
|
269
|
+
this.logger.info('Webhook bot update received', { workspaceId, botType: bot.botType, action, enabled: bot.enabled });
|
|
270
|
+
if (bot.botType === 'orchestrator') {
|
|
271
|
+
if (action === 'remove') {
|
|
272
|
+
// Orchestrator disabled - stop daemons, don't restart
|
|
273
|
+
this.logger.info('Orchestrator disabled via webhook, stopping daemons');
|
|
274
|
+
this.daemonManager?.stopAll().catch(err => {
|
|
275
|
+
this.logger.error('Failed to stop daemons after orchestrator removal', err);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Orchestrator added/updated - restart daemons
|
|
280
|
+
this.logger.info('Orchestrator config changed via webhook, triggering daemon restart');
|
|
281
|
+
this.attemptDaemonRestart(0, workspaceId).catch(err => {
|
|
282
|
+
this.logger.error('Failed to restart daemon after orchestrator webhook update', err);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Specialist changed - hot-reload without losing orchestrator context
|
|
288
|
+
this.logger.info('Specialist config changed via webhook, hot-reloading', {
|
|
289
|
+
botType: bot.botType,
|
|
290
|
+
enabled: bot.enabled
|
|
291
|
+
});
|
|
292
|
+
if (this.daemonManager && bot.botType) {
|
|
293
|
+
this.daemonManager.hotReloadSpecialist(bot.email, bot.password, bot.botType, bot.enabled, bot.userId || undefined).then(success => {
|
|
294
|
+
if (success) {
|
|
295
|
+
this.logger.info('Specialist hot-reloaded successfully', {
|
|
296
|
+
botType: bot.botType,
|
|
297
|
+
enabled: bot.enabled
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.logger.warn('Specialist hot-reload failed', { botType: bot.botType });
|
|
302
|
+
}
|
|
303
|
+
}).catch(err => {
|
|
304
|
+
this.logger.error('Error during specialist hot-reload', err);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
180
309
|
// Start periodic status logging (every 60s)
|
|
181
310
|
this.statusLogInterval = this.daemonManager.startStatusLogging(60000);
|
|
182
311
|
this.logger.info('Daemon status logging started (every 60s)');
|
|
@@ -198,6 +327,8 @@ class Core {
|
|
|
198
327
|
if (this.bugReportsModule) {
|
|
199
328
|
await this.bugReportsModule.stop();
|
|
200
329
|
}
|
|
330
|
+
// Cleanup bot config (clear timers and callbacks)
|
|
331
|
+
(0, bot_config_1.cleanupBotConfig)();
|
|
201
332
|
if (this.daemonManager) {
|
|
202
333
|
this.daemonManager.stopAll();
|
|
203
334
|
this.logger.info('Chat Agent Daemon stopped');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discussion Lock Registry
|
|
3
|
+
*
|
|
4
|
+
* Prevents multiple bots from responding to the same discussion.
|
|
5
|
+
* When a specialist bot (like Giuseppe) is handling a discussion,
|
|
6
|
+
* it acquires a lock. Other bots (like Orchestrator) check the lock
|
|
7
|
+
* before responding.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Acquire a lock on a discussion
|
|
11
|
+
* @param discussionId - The discussion to lock
|
|
12
|
+
* @param botName - Name of the bot acquiring the lock (for logging)
|
|
13
|
+
* @param ttlMs - Lock duration in milliseconds (default: 5 minutes)
|
|
14
|
+
* @returns true if lock acquired, false if already locked by another bot
|
|
15
|
+
*/
|
|
16
|
+
export declare function acquireDiscussionLock(discussionId: string, botName: string, ttlMs?: number): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Release a lock on a discussion
|
|
19
|
+
* @param discussionId - The discussion to unlock
|
|
20
|
+
* @param botName - Name of the bot releasing (must match acquirer)
|
|
21
|
+
*/
|
|
22
|
+
export declare function releaseDiscussionLock(discussionId: string, botName: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Check if a discussion is locked by another bot
|
|
25
|
+
* @param discussionId - The discussion to check
|
|
26
|
+
* @param myBotName - Name of the checking bot (own locks don't block)
|
|
27
|
+
* @returns true if locked by ANOTHER bot, false if free or own lock
|
|
28
|
+
*/
|
|
29
|
+
export declare function isDiscussionLocked(discussionId: string, myBotName: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Clean up expired locks (call periodically)
|
|
32
|
+
*/
|
|
33
|
+
export declare function cleanupExpiredLocks(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Get current lock status (for debugging)
|
|
36
|
+
*/
|
|
37
|
+
export declare function getLockStatus(): Map<string, {
|
|
38
|
+
botName: string;
|
|
39
|
+
acquiredAt: number;
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
}>;
|
|
42
|
+
//# sourceMappingURL=discussion-lock.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discussion Lock Registry
|
|
4
|
+
*
|
|
5
|
+
* Prevents multiple bots from responding to the same discussion.
|
|
6
|
+
* When a specialist bot (like Giuseppe) is handling a discussion,
|
|
7
|
+
* it acquires a lock. Other bots (like Orchestrator) check the lock
|
|
8
|
+
* before responding.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.acquireDiscussionLock = acquireDiscussionLock;
|
|
12
|
+
exports.releaseDiscussionLock = releaseDiscussionLock;
|
|
13
|
+
exports.isDiscussionLocked = isDiscussionLocked;
|
|
14
|
+
exports.cleanupExpiredLocks = cleanupExpiredLocks;
|
|
15
|
+
exports.getLockStatus = getLockStatus;
|
|
16
|
+
const logger_1 = require("./logger");
|
|
17
|
+
const logger = (0, logger_1.createLogger)({ component: 'discussion-lock' });
|
|
18
|
+
// Singleton map of discussionId -> { botName, acquiredAt, expiresAt }
|
|
19
|
+
const locks = new Map();
|
|
20
|
+
// Default lock TTL: 5 minutes (allows for LLM processing time)
|
|
21
|
+
const DEFAULT_LOCK_TTL_MS = 5 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* Acquire a lock on a discussion
|
|
24
|
+
* @param discussionId - The discussion to lock
|
|
25
|
+
* @param botName - Name of the bot acquiring the lock (for logging)
|
|
26
|
+
* @param ttlMs - Lock duration in milliseconds (default: 5 minutes)
|
|
27
|
+
* @returns true if lock acquired, false if already locked by another bot
|
|
28
|
+
*/
|
|
29
|
+
function acquireDiscussionLock(discussionId, botName, ttlMs = DEFAULT_LOCK_TTL_MS) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const existing = locks.get(discussionId);
|
|
32
|
+
// Check if existing lock is still valid
|
|
33
|
+
if (existing && existing.expiresAt > now) {
|
|
34
|
+
if (existing.botName === botName) {
|
|
35
|
+
// Same bot - extend the lock
|
|
36
|
+
existing.expiresAt = now + ttlMs;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// Different bot has the lock
|
|
40
|
+
logger.debug('Discussion already locked', {
|
|
41
|
+
discussionId,
|
|
42
|
+
lockedBy: existing.botName,
|
|
43
|
+
requestedBy: botName
|
|
44
|
+
});
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Acquire lock
|
|
48
|
+
locks.set(discussionId, {
|
|
49
|
+
botName,
|
|
50
|
+
acquiredAt: now,
|
|
51
|
+
expiresAt: now + ttlMs
|
|
52
|
+
});
|
|
53
|
+
logger.debug('Discussion lock acquired', { discussionId, botName, ttlMs });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Release a lock on a discussion
|
|
58
|
+
* @param discussionId - The discussion to unlock
|
|
59
|
+
* @param botName - Name of the bot releasing (must match acquirer)
|
|
60
|
+
*/
|
|
61
|
+
function releaseDiscussionLock(discussionId, botName) {
|
|
62
|
+
const existing = locks.get(discussionId);
|
|
63
|
+
if (existing && existing.botName === botName) {
|
|
64
|
+
locks.delete(discussionId);
|
|
65
|
+
logger.debug('Discussion lock released', { discussionId, botName });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a discussion is locked by another bot
|
|
70
|
+
* @param discussionId - The discussion to check
|
|
71
|
+
* @param myBotName - Name of the checking bot (own locks don't block)
|
|
72
|
+
* @returns true if locked by ANOTHER bot, false if free or own lock
|
|
73
|
+
*/
|
|
74
|
+
function isDiscussionLocked(discussionId, myBotName) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const existing = locks.get(discussionId);
|
|
77
|
+
// No lock or expired
|
|
78
|
+
if (!existing || existing.expiresAt <= now) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// Own lock doesn't block
|
|
82
|
+
if (existing.botName === myBotName) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clean up expired locks (call periodically)
|
|
89
|
+
*/
|
|
90
|
+
function cleanupExpiredLocks() {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
let cleaned = 0;
|
|
93
|
+
for (const [discussionId, lock] of locks.entries()) {
|
|
94
|
+
if (lock.expiresAt <= now) {
|
|
95
|
+
locks.delete(discussionId);
|
|
96
|
+
cleaned++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (cleaned > 0) {
|
|
100
|
+
logger.debug('Cleaned up expired locks', { count: cleaned });
|
|
101
|
+
}
|
|
102
|
+
return cleaned;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get current lock status (for debugging)
|
|
106
|
+
*/
|
|
107
|
+
function getLockStatus() {
|
|
108
|
+
return new Map(locks);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=discussion-lock.js.map
|
|
@@ -101,9 +101,9 @@ class UserContextCache {
|
|
|
101
101
|
const client = await (0, hailer_clients_1.createHailerClientByApiKey)(apiKey);
|
|
102
102
|
// Create API client (host extracted from client automatically)
|
|
103
103
|
const hailer = new index_1.HailerApiClient(client);
|
|
104
|
-
// Fetch user's workspace initialization data
|
|
104
|
+
// Fetch user's workspace initialization data (including teams for activity creation)
|
|
105
105
|
const init = await client.socket.request('v2.core.init', [
|
|
106
|
-
['processes', 'users', 'network', 'networks']
|
|
106
|
+
['processes', 'users', 'network', 'networks', 'teams']
|
|
107
107
|
]);
|
|
108
108
|
// Create workspace cache from init data
|
|
109
109
|
const appConfig = (0, config_1.createApplicationConfig)();
|
|
@@ -24,6 +24,7 @@ export declare class HailerClientManager {
|
|
|
24
24
|
private socketClient;
|
|
25
25
|
private restClient;
|
|
26
26
|
private signalHandlers;
|
|
27
|
+
private signalsInitialized;
|
|
27
28
|
constructor(host: string, username: string, password: string);
|
|
28
29
|
connect(): Promise<HailerClient>;
|
|
29
30
|
private setupSignalHandling;
|
|
@@ -42,8 +43,22 @@ export declare const createHailerClientByApiKey: (apiKey: string) => Promise<Hai
|
|
|
42
43
|
* Disconnect client by API key (new unified approach)
|
|
43
44
|
*/
|
|
44
45
|
export declare const disconnectHailerClientByApiKey: (apiKey: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Clear all connections - forces fresh reconnect on next request
|
|
48
|
+
* Use this when you need to refresh stale server-side data (e.g., after template install)
|
|
49
|
+
*/
|
|
50
|
+
export declare const clearAllConnections: () => void;
|
|
45
51
|
/**
|
|
46
52
|
* Subscribe to signals for a client connection
|
|
47
53
|
*/
|
|
48
54
|
export declare const subscribeToSignal: (apiKey: string, eventType: SignalType | string, handler: SignalHandler) => (() => void) | null;
|
|
55
|
+
/**
|
|
56
|
+
* Register new bot credentials for dynamic bot creation
|
|
57
|
+
* Returns an API key that can be used to create a connection
|
|
58
|
+
*/
|
|
59
|
+
export declare function registerBotCredentials(botId: string, email: string, password: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Unregister bot credentials and disconnect
|
|
62
|
+
*/
|
|
63
|
+
export declare function unregisterBotCredentials(apiKey: string): void;
|
|
49
64
|
//# sourceMappingURL=hailer-clients.d.ts.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.subscribeToSignal = exports.disconnectHailerClientByApiKey = exports.createHailerClientByApiKey = exports.HailerClientManager = void 0;
|
|
3
|
+
exports.subscribeToSignal = exports.clearAllConnections = exports.disconnectHailerClientByApiKey = exports.createHailerClientByApiKey = exports.HailerClientManager = void 0;
|
|
4
4
|
exports.getCurrentUserId = getCurrentUserId;
|
|
5
|
+
exports.registerBotCredentials = registerBotCredentials;
|
|
6
|
+
exports.unregisterBotCredentials = unregisterBotCredentials;
|
|
5
7
|
const cli_1 = require("@hailer/cli");
|
|
6
8
|
const auth_1 = require("./auth");
|
|
7
9
|
const logger_1 = require("../lib/logger");
|
|
@@ -74,6 +76,7 @@ class HailerClientManager {
|
|
|
74
76
|
socketClient = null;
|
|
75
77
|
restClient = null;
|
|
76
78
|
signalHandlers = new Map();
|
|
79
|
+
signalsInitialized = false;
|
|
77
80
|
constructor(host, username, password) {
|
|
78
81
|
this.host = host;
|
|
79
82
|
this.username = username;
|
|
@@ -88,14 +91,35 @@ class HailerClientManager {
|
|
|
88
91
|
password: this.password,
|
|
89
92
|
...(isLocalDev && { rejectUnauthorized: false }), // Add for local dev with self-signed certs
|
|
90
93
|
};
|
|
94
|
+
// Track timeout for proper cleanup
|
|
95
|
+
let timeoutId = null;
|
|
96
|
+
let timeoutCleared = false;
|
|
91
97
|
try {
|
|
92
|
-
// Create socket client using @hailer/cli with timeout
|
|
98
|
+
// Create socket client using @hailer/cli with cancellable timeout
|
|
93
99
|
this.socketClient = (await Promise.race([
|
|
94
|
-
cli_1.Client.create(clientOptions)
|
|
95
|
-
|
|
100
|
+
cli_1.Client.create(clientOptions).then(client => {
|
|
101
|
+
// Clear timeout on success
|
|
102
|
+
if (timeoutId && !timeoutCleared) {
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
timeoutCleared = true;
|
|
105
|
+
}
|
|
106
|
+
return client;
|
|
107
|
+
}),
|
|
108
|
+
new Promise((_, reject) => {
|
|
109
|
+
timeoutId = setTimeout(() => {
|
|
110
|
+
if (!timeoutCleared) {
|
|
111
|
+
reject(new Error(`Timeout connecting to: ${this.host}`));
|
|
112
|
+
}
|
|
113
|
+
}, 30000);
|
|
114
|
+
}),
|
|
96
115
|
]));
|
|
97
116
|
}
|
|
98
117
|
catch (error) {
|
|
118
|
+
// Ensure timeout is cleared on error too
|
|
119
|
+
if (timeoutId && !timeoutCleared) {
|
|
120
|
+
clearTimeout(timeoutId);
|
|
121
|
+
timeoutCleared = true;
|
|
122
|
+
}
|
|
99
123
|
logger.error('Failed to create socket client', error, { username: this.username });
|
|
100
124
|
throw error;
|
|
101
125
|
}
|
|
@@ -123,10 +147,43 @@ class HailerClientManager {
|
|
|
123
147
|
};
|
|
124
148
|
}
|
|
125
149
|
setupSignalHandling() {
|
|
126
|
-
if (!this.socketClient)
|
|
150
|
+
if (!this.socketClient || this.signalsInitialized)
|
|
127
151
|
return;
|
|
152
|
+
this.signalsInitialized = true;
|
|
153
|
+
// Connection lifecycle logging - helps diagnose bot death issues
|
|
154
|
+
this.socketClient.on("disconnect", (reason) => {
|
|
155
|
+
logger.warn('Socket disconnected', {
|
|
156
|
+
reason,
|
|
157
|
+
username: this.username,
|
|
158
|
+
willReconnect: reason !== 'io server disconnect' && reason !== 'io client disconnect'
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
this.socketClient.on("reconnect", () => {
|
|
162
|
+
logger.info('Socket reconnected and session resumed', { username: this.username });
|
|
163
|
+
});
|
|
164
|
+
this.socketClient.on("connect", () => {
|
|
165
|
+
logger.info('Socket connected', { username: this.username });
|
|
166
|
+
});
|
|
167
|
+
this.socketClient.on("connect_error", (error) => {
|
|
168
|
+
logger.error('Socket connection error', { error: error.message, username: this.username });
|
|
169
|
+
});
|
|
170
|
+
this.socketClient.on("reconnect_attempt", (attempt) => {
|
|
171
|
+
logger.info('Socket reconnection attempt', { attempt, username: this.username });
|
|
172
|
+
});
|
|
173
|
+
this.socketClient.on("reconnect_failed", () => {
|
|
174
|
+
logger.error('Socket reconnection failed permanently', { username: this.username });
|
|
175
|
+
});
|
|
128
176
|
this.socketClient.on("signals", (signal) => {
|
|
129
177
|
const [eventType, eventData] = signal;
|
|
178
|
+
// Debug: log all incoming signals to see what we're receiving
|
|
179
|
+
if (eventType === 'messenger.new') {
|
|
180
|
+
logger.debug('HailerClientManager received messenger.new signal', {
|
|
181
|
+
eventType,
|
|
182
|
+
discussion: eventData.discussion,
|
|
183
|
+
handlersRegistered: this.signalHandlers.has(eventType),
|
|
184
|
+
handlerCount: this.signalHandlers.get(eventType)?.length || 0,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
130
187
|
// Dispatch to registered handlers
|
|
131
188
|
const handlers = this.signalHandlers.get(eventType);
|
|
132
189
|
if (handlers) {
|
|
@@ -165,6 +222,7 @@ class HailerClientManager {
|
|
|
165
222
|
}
|
|
166
223
|
this.restClient = null;
|
|
167
224
|
this.signalHandlers.clear();
|
|
225
|
+
this.signalsInitialized = false;
|
|
168
226
|
}
|
|
169
227
|
isConnected() {
|
|
170
228
|
return this.socketClient !== null && this.restClient !== null;
|
|
@@ -223,7 +281,7 @@ const createHailerClientByApiKey = async (apiKey) => {
|
|
|
223
281
|
// Create new connection
|
|
224
282
|
logger.info('Creating new connection', {
|
|
225
283
|
apiKey: apiKey.substring(0, 8) + '...',
|
|
226
|
-
email: account.email,
|
|
284
|
+
email: (0, config_1.maskEmail)(account.email),
|
|
227
285
|
host: account.apiBaseUrl
|
|
228
286
|
});
|
|
229
287
|
clientManager = new HailerClientManager(account.apiBaseUrl, account.email, account.password);
|
|
@@ -243,6 +301,19 @@ const disconnectHailerClientByApiKey = (apiKey) => {
|
|
|
243
301
|
}
|
|
244
302
|
};
|
|
245
303
|
exports.disconnectHailerClientByApiKey = disconnectHailerClientByApiKey;
|
|
304
|
+
/**
|
|
305
|
+
* Clear all connections - forces fresh reconnect on next request
|
|
306
|
+
* Use this when you need to refresh stale server-side data (e.g., after template install)
|
|
307
|
+
*/
|
|
308
|
+
const clearAllConnections = () => {
|
|
309
|
+
const count = connectionPool.size;
|
|
310
|
+
for (const [, clientManager] of connectionPool.entries()) {
|
|
311
|
+
clientManager.disconnect();
|
|
312
|
+
}
|
|
313
|
+
connectionPool.clear();
|
|
314
|
+
logger.info('Cleared Hailer connections', { count });
|
|
315
|
+
};
|
|
316
|
+
exports.clearAllConnections = clearAllConnections;
|
|
246
317
|
/**
|
|
247
318
|
* Subscribe to signals for a client connection
|
|
248
319
|
*/
|
|
@@ -258,4 +329,27 @@ const subscribeToSignal = (apiKey, eventType, handler) => {
|
|
|
258
329
|
};
|
|
259
330
|
};
|
|
260
331
|
exports.subscribeToSignal = subscribeToSignal;
|
|
332
|
+
/**
|
|
333
|
+
* Register new bot credentials for dynamic bot creation
|
|
334
|
+
* Returns an API key that can be used to create a connection
|
|
335
|
+
*/
|
|
336
|
+
function registerBotCredentials(botId, email, password) {
|
|
337
|
+
const apiKey = `bot-${botId}-${Date.now()}`;
|
|
338
|
+
// Add to environment CLIENT_CONFIGS
|
|
339
|
+
config_1.environment.CLIENT_CONFIGS[apiKey] = {
|
|
340
|
+
email,
|
|
341
|
+
password,
|
|
342
|
+
apiBaseUrl: 'https://api.hailer.com',
|
|
343
|
+
};
|
|
344
|
+
logger.info('Bot credentials registered', { botId });
|
|
345
|
+
return apiKey;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Unregister bot credentials and disconnect
|
|
349
|
+
*/
|
|
350
|
+
function unregisterBotCredentials(apiKey) {
|
|
351
|
+
delete config_1.environment.CLIENT_CONFIGS[apiKey];
|
|
352
|
+
(0, exports.disconnectHailerClientByApiKey)(apiKey);
|
|
353
|
+
logger.info('Bot credentials unregistered');
|
|
354
|
+
}
|
|
261
355
|
//# sourceMappingURL=hailer-clients.js.map
|
|
@@ -3,7 +3,7 @@ import { HailerClient } from './hailer-clients';
|
|
|
3
3
|
/**
|
|
4
4
|
* Signal types that Hailer emits via socket.io
|
|
5
5
|
*/
|
|
6
|
-
export type HailerSignalType = 'activities.updated' | 'activities.created' | 'activities.deleted' | 'discussion.message' | 'messenger.new' | 'user.joined' | 'user.left' | 'workspace.updated' | 'process.updated' | 'cache.invalidate';
|
|
6
|
+
export type HailerSignalType = 'activities.updated' | 'activities.created' | 'activities.deleted' | 'discussion.message' | 'messenger.new' | 'user.joined' | 'user.left' | 'workspace.updated' | 'process.updated' | 'cache.invalidate' | 'company.new_invitation';
|
|
7
7
|
/** Raw signal from Hailer socket - tuple of [eventType, eventData] */
|
|
8
8
|
export type HailerSocketSignal = [string, Record<string, unknown>];
|
|
9
9
|
/** Message object nested in signal data */
|
|
@@ -14,6 +14,13 @@ export interface HailerSignalMessage {
|
|
|
14
14
|
user?: string;
|
|
15
15
|
userName?: string;
|
|
16
16
|
}
|
|
17
|
+
/** Metadata for activities.updated signals - contains phase change information */
|
|
18
|
+
export interface ActivityUpdatedMeta {
|
|
19
|
+
processId: string;
|
|
20
|
+
phase: string;
|
|
21
|
+
prevPhase?: string;
|
|
22
|
+
activity_id: string | string[];
|
|
23
|
+
}
|
|
17
24
|
/** Signal data varies by type - common fields for type narrowing */
|
|
18
25
|
export interface HailerSignalData {
|
|
19
26
|
_id?: string;
|
|
@@ -26,6 +33,7 @@ export interface HailerSignalData {
|
|
|
26
33
|
discussion?: string;
|
|
27
34
|
uid?: string;
|
|
28
35
|
message?: HailerSignalMessage;
|
|
36
|
+
meta?: ActivityUpdatedMeta;
|
|
29
37
|
[key: string]: unknown;
|
|
30
38
|
}
|
|
31
39
|
export interface HailerSignal {
|
|
@@ -46,7 +54,13 @@ export declare class SignalHandler {
|
|
|
46
54
|
private subscriptions;
|
|
47
55
|
private signalHistory;
|
|
48
56
|
private maxHistorySize;
|
|
57
|
+
private static handlersBySocketId;
|
|
49
58
|
constructor(client: HailerClient, workspaceCache?: WorkspaceCache | undefined);
|
|
59
|
+
/**
|
|
60
|
+
* Get or create a SignalHandler for a client (prevents duplicate listeners)
|
|
61
|
+
*/
|
|
62
|
+
static getOrCreate(client: HailerClient, workspaceCache?: WorkspaceCache): SignalHandler;
|
|
63
|
+
private signalListener;
|
|
50
64
|
private initializeSignalHandling;
|
|
51
65
|
private handleIncomingSignal;
|
|
52
66
|
private addToHistory;
|
|
@@ -58,14 +72,11 @@ export declare class SignalHandler {
|
|
|
58
72
|
private handleWorkspaceUpdated;
|
|
59
73
|
private handleMessengerNew;
|
|
60
74
|
private handleCacheInvalidate;
|
|
75
|
+
private handleNewInvitation;
|
|
61
76
|
subscribe(id: string, types: HailerSignalType[], handler: (signal: HailerSignal) => void, workspaceId?: string): void;
|
|
62
77
|
unsubscribe(id: string): boolean;
|
|
63
78
|
getSignalHistory(types?: HailerSignalType[], limit?: number, workspaceId?: string): HailerSignal[];
|
|
64
79
|
getActiveSubscriptions(): SignalSubscription[];
|
|
65
80
|
clearHistory(): void;
|
|
66
|
-
createMcpSignalTools(server: {
|
|
67
|
-
tool: (...args: unknown[]) => void;
|
|
68
|
-
}): void;
|
|
69
81
|
}
|
|
70
|
-
export declare const createSignalHandler: (clients: HailerClient, workspaceCache?: WorkspaceCache) => SignalHandler;
|
|
71
82
|
//# sourceMappingURL=signal-handler.d.ts.map
|