mach6-core 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/agent/context-monitor.d.ts +40 -0
- package/dist/agent/context-monitor.d.ts.map +1 -0
- package/dist/agent/context-monitor.js +138 -0
- package/dist/agent/context-monitor.js.map +1 -0
- package/dist/agent/context.d.ts +8 -0
- package/dist/agent/context.d.ts.map +1 -0
- package/dist/agent/context.js +197 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/runner.d.ts +42 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +168 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/system-prompt.d.ts +10 -0
- package/dist/agent/system-prompt.d.ts.map +1 -0
- package/dist/agent/system-prompt.js +120 -0
- package/dist/agent/system-prompt.js.map +1 -0
- package/dist/boot/sequence.d.ts +34 -0
- package/dist/boot/sequence.d.ts.map +1 -0
- package/dist/boot/sequence.js +103 -0
- package/dist/boot/sequence.js.map +1 -0
- package/dist/channels/__test__/integration.test.d.ts +8 -0
- package/dist/channels/__test__/integration.test.d.ts.map +1 -0
- package/dist/channels/__test__/integration.test.js +221 -0
- package/dist/channels/__test__/integration.test.js.map +1 -0
- package/dist/channels/adapter.d.ts +53 -0
- package/dist/channels/adapter.d.ts.map +1 -0
- package/dist/channels/adapter.js +164 -0
- package/dist/channels/adapter.js.map +1 -0
- package/dist/channels/adapters/discord.d.ts +49 -0
- package/dist/channels/adapters/discord.d.ts.map +1 -0
- package/dist/channels/adapters/discord.js +382 -0
- package/dist/channels/adapters/discord.js.map +1 -0
- package/dist/channels/adapters/whatsapp.d.ts +59 -0
- package/dist/channels/adapters/whatsapp.d.ts.map +1 -0
- package/dist/channels/adapters/whatsapp.js +503 -0
- package/dist/channels/adapters/whatsapp.js.map +1 -0
- package/dist/channels/bus.d.ts +55 -0
- package/dist/channels/bus.d.ts.map +1 -0
- package/dist/channels/bus.js +307 -0
- package/dist/channels/bus.js.map +1 -0
- package/dist/channels/formatter.d.ts +16 -0
- package/dist/channels/formatter.d.ts.map +1 -0
- package/dist/channels/formatter.js +226 -0
- package/dist/channels/formatter.js.map +1 -0
- package/dist/channels/presence.d.ts +67 -0
- package/dist/channels/presence.d.ts.map +1 -0
- package/dist/channels/presence.js +209 -0
- package/dist/channels/presence.js.map +1 -0
- package/dist/channels/registry.d.ts +57 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +159 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/router.d.ts +49 -0
- package/dist/channels/router.d.ts.map +1 -0
- package/dist/channels/router.js +244 -0
- package/dist/channels/router.js.map +1 -0
- package/dist/channels/types.d.ts +279 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +67 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/cli/brand.d.ts +71 -0
- package/dist/cli/brand.d.ts.map +1 -0
- package/dist/cli/brand.js +194 -0
- package/dist/cli/brand.js.map +1 -0
- package/dist/cli/wizard.d.ts +8 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +520 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/config/config.d.ts +47 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +63 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/validator.d.ts +18 -0
- package/dist/config/validator.d.ts.map +1 -0
- package/dist/config/validator.js +92 -0
- package/dist/config/validator.js.map +1 -0
- package/dist/cron/budget.d.ts +39 -0
- package/dist/cron/budget.d.ts.map +1 -0
- package/dist/cron/budget.js +98 -0
- package/dist/cron/budget.js.map +1 -0
- package/dist/formatters/markdown.d.ts +6 -0
- package/dist/formatters/markdown.d.ts.map +1 -0
- package/dist/formatters/markdown.js +85 -0
- package/dist/formatters/markdown.js.map +1 -0
- package/dist/gateway/daemon.d.ts +97 -0
- package/dist/gateway/daemon.d.ts.map +1 -0
- package/dist/gateway/daemon.js +772 -0
- package/dist/gateway/daemon.js.map +1 -0
- package/dist/heartbeat/scheduler.d.ts +45 -0
- package/dist/heartbeat/scheduler.d.ts.map +1 -0
- package/dist/heartbeat/scheduler.js +102 -0
- package/dist/heartbeat/scheduler.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +305 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/integrity.d.ts +37 -0
- package/dist/memory/integrity.d.ts.map +1 -0
- package/dist/memory/integrity.js +108 -0
- package/dist/memory/integrity.js.map +1 -0
- package/dist/providers/anthropic.d.ts +3 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +232 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/diagnostics.d.ts +18 -0
- package/dist/providers/diagnostics.d.ts.map +1 -0
- package/dist/providers/diagnostics.js +87 -0
- package/dist/providers/diagnostics.js.map +1 -0
- package/dist/providers/github-copilot.d.ts +3 -0
- package/dist/providers/github-copilot.d.ts.map +1 -0
- package/dist/providers/github-copilot.js +145 -0
- package/dist/providers/github-copilot.js.map +1 -0
- package/dist/providers/gladius.d.ts +3 -0
- package/dist/providers/gladius.d.ts.map +1 -0
- package/dist/providers/gladius.js +16 -0
- package/dist/providers/gladius.js.map +1 -0
- package/dist/providers/openai.d.ts +3 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +161 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/retry.d.ts +2 -0
- package/dist/providers/retry.d.ts.map +1 -0
- package/dist/providers/retry.js +50 -0
- package/dist/providers/retry.js.map +1 -0
- package/dist/providers/types.d.ts +78 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/security/sanitizer.d.ts +26 -0
- package/dist/security/sanitizer.d.ts.map +1 -0
- package/dist/security/sanitizer.js +115 -0
- package/dist/security/sanitizer.js.map +1 -0
- package/dist/sessions/manager.d.ts +40 -0
- package/dist/sessions/manager.d.ts.map +1 -0
- package/dist/sessions/manager.js +255 -0
- package/dist/sessions/manager.js.map +1 -0
- package/dist/sessions/queue.d.ts +41 -0
- package/dist/sessions/queue.d.ts.map +1 -0
- package/dist/sessions/queue.js +93 -0
- package/dist/sessions/queue.js.map +1 -0
- package/dist/sessions/store.d.ts +12 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +74 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/sub-agent.d.ts +18 -0
- package/dist/sessions/sub-agent.d.ts.map +1 -0
- package/dist/sessions/sub-agent.js +117 -0
- package/dist/sessions/sub-agent.js.map +1 -0
- package/dist/sessions/types.d.ts +48 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +3 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/test/channel-integration.d.ts +10 -0
- package/dist/test/channel-integration.d.ts.map +1 -0
- package/dist/test/channel-integration.js +226 -0
- package/dist/test/channel-integration.js.map +1 -0
- package/dist/test/prompt-test.d.ts +2 -0
- package/dist/test/prompt-test.d.ts.map +1 -0
- package/dist/test/prompt-test.js +33 -0
- package/dist/test/prompt-test.js.map +1 -0
- package/dist/test/smoke.d.ts +8 -0
- package/dist/test/smoke.d.ts.map +1 -0
- package/dist/test/smoke.js +134 -0
- package/dist/test/smoke.js.map +1 -0
- package/dist/tools/builtin/comb.d.ts +4 -0
- package/dist/tools/builtin/comb.d.ts.map +1 -0
- package/dist/tools/builtin/comb.js +50 -0
- package/dist/tools/builtin/comb.js.map +1 -0
- package/dist/tools/builtin/edit.d.ts +3 -0
- package/dist/tools/builtin/edit.d.ts.map +1 -0
- package/dist/tools/builtin/edit.js +42 -0
- package/dist/tools/builtin/edit.js.map +1 -0
- package/dist/tools/builtin/exec.d.ts +3 -0
- package/dist/tools/builtin/exec.d.ts.map +1 -0
- package/dist/tools/builtin/exec.js +75 -0
- package/dist/tools/builtin/exec.js.map +1 -0
- package/dist/tools/builtin/image.d.ts +3 -0
- package/dist/tools/builtin/image.d.ts.map +1 -0
- package/dist/tools/builtin/image.js +208 -0
- package/dist/tools/builtin/image.js.map +1 -0
- package/dist/tools/builtin/memory.d.ts +3 -0
- package/dist/tools/builtin/memory.d.ts.map +1 -0
- package/dist/tools/builtin/memory.js +36 -0
- package/dist/tools/builtin/memory.js.map +1 -0
- package/dist/tools/builtin/message.d.ts +13 -0
- package/dist/tools/builtin/message.d.ts.map +1 -0
- package/dist/tools/builtin/message.js +330 -0
- package/dist/tools/builtin/message.js.map +1 -0
- package/dist/tools/builtin/process.d.ts +43 -0
- package/dist/tools/builtin/process.d.ts.map +1 -0
- package/dist/tools/builtin/process.js +178 -0
- package/dist/tools/builtin/process.js.map +1 -0
- package/dist/tools/builtin/read.d.ts +3 -0
- package/dist/tools/builtin/read.d.ts.map +1 -0
- package/dist/tools/builtin/read.js +36 -0
- package/dist/tools/builtin/read.js.map +1 -0
- package/dist/tools/builtin/spawn.d.ts +8 -0
- package/dist/tools/builtin/spawn.d.ts.map +1 -0
- package/dist/tools/builtin/spawn.js +90 -0
- package/dist/tools/builtin/spawn.js.map +1 -0
- package/dist/tools/builtin/tts.d.ts +3 -0
- package/dist/tools/builtin/tts.d.ts.map +1 -0
- package/dist/tools/builtin/tts.js +77 -0
- package/dist/tools/builtin/tts.js.map +1 -0
- package/dist/tools/builtin/web-fetch.d.ts +3 -0
- package/dist/tools/builtin/web-fetch.d.ts.map +1 -0
- package/dist/tools/builtin/web-fetch.js +46 -0
- package/dist/tools/builtin/web-fetch.js.map +1 -0
- package/dist/tools/builtin/write.d.ts +3 -0
- package/dist/tools/builtin/write.d.ts.map +1 -0
- package/dist/tools/builtin/write.js +31 -0
- package/dist/tools/builtin/write.js.map +1 -0
- package/dist/tools/mcp-bridge.d.ts +42 -0
- package/dist/tools/mcp-bridge.d.ts.map +1 -0
- package/dist/tools/mcp-bridge.js +198 -0
- package/dist/tools/mcp-bridge.js.map +1 -0
- package/dist/tools/policy.d.ts +57 -0
- package/dist/tools/policy.d.ts.map +1 -0
- package/dist/tools/policy.js +106 -0
- package/dist/tools/policy.js.map +1 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +41 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/sandbox.d.ts +91 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +279 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/types.d.ts +23 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +3 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/web/http-api.d.ts +61 -0
- package/dist/web/http-api.d.ts.map +1 -0
- package/dist/web/http-api.js +200 -0
- package/dist/web/http-api.js.map +1 -0
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +396 -0
- package/dist/web/server.js.map +1 -0
- package/mach6.example.json +50 -0
- package/package.json +58 -0
- package/web/index.html +1370 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mach6 — Gateway Daemon
|
|
3
|
+
*
|
|
4
|
+
* The persistent process. Manages channel lifecycle, agent sessions,
|
|
5
|
+
* signal handling, graceful shutdown, and hot-reload.
|
|
6
|
+
*
|
|
7
|
+
* Mach6 AI Gateway — sovereign engine.
|
|
8
|
+
*/
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
// Use global process (don't import — it shadows signal handlers)
|
|
13
|
+
import { palette, gradient, versionBanner, kvLine, ok, warn, info, divider, } from '../cli/brand.js';
|
|
14
|
+
import { ChannelRegistry } from '../channels/registry.js';
|
|
15
|
+
import { DiscordAdapter } from '../channels/adapters/discord.js';
|
|
16
|
+
import { WhatsAppAdapter } from '../channels/adapters/whatsapp.js';
|
|
17
|
+
import { ToolRegistry } from '../tools/registry.js';
|
|
18
|
+
import { presenceManager } from '../channels/presence.js';
|
|
19
|
+
import { HeartbeatScheduler } from '../heartbeat/scheduler.js';
|
|
20
|
+
import { readTool } from '../tools/builtin/read.js';
|
|
21
|
+
import { writeTool } from '../tools/builtin/write.js';
|
|
22
|
+
import { execTool } from '../tools/builtin/exec.js';
|
|
23
|
+
import { editTool } from '../tools/builtin/edit.js';
|
|
24
|
+
import { imageTool } from '../tools/builtin/image.js';
|
|
25
|
+
import { processStartTool, processPollTool, processKillTool, processListTool, } from '../tools/builtin/process.js';
|
|
26
|
+
import { ttsTool } from '../tools/builtin/tts.js';
|
|
27
|
+
import { webFetchTool } from '../tools/builtin/web-fetch.js';
|
|
28
|
+
import { memorySearchTool } from '../tools/builtin/memory.js';
|
|
29
|
+
import { combRecallTool, combStageTool } from '../tools/builtin/comb.js';
|
|
30
|
+
import { createSpawnTool, createSubAgentStatusTool } from '../tools/builtin/spawn.js';
|
|
31
|
+
import { SubAgentManager } from '../sessions/sub-agent.js';
|
|
32
|
+
import { createMessageTool, createTypingTool, createPresenceTool, createDeleteMessageTool, createMarkReadTool } from '../tools/builtin/message.js';
|
|
33
|
+
import { SessionManager } from '../sessions/manager.js';
|
|
34
|
+
import { buildSystemPrompt } from '../agent/system-prompt.js';
|
|
35
|
+
import { runAgent } from '../agent/runner.js';
|
|
36
|
+
import { loadConfig } from '../config/config.js';
|
|
37
|
+
import { anthropicProvider } from '../providers/anthropic.js';
|
|
38
|
+
import { openaiProvider } from '../providers/openai.js';
|
|
39
|
+
import { githubCopilotProvider } from '../providers/github-copilot.js';
|
|
40
|
+
import { gladiusProvider } from '../providers/gladius.js';
|
|
41
|
+
import { createSandboxedRegistry } from '../tools/sandbox.js';
|
|
42
|
+
import { HttpApiServer } from '../web/http-api.js';
|
|
43
|
+
import { McpBridge } from '../tools/mcp-bridge.js';
|
|
44
|
+
// ─── Provider Registry ─────────────────────────────────────────────────────
|
|
45
|
+
const PROVIDERS = new Map([
|
|
46
|
+
['anthropic', anthropicProvider],
|
|
47
|
+
['openai', openaiProvider],
|
|
48
|
+
['github-copilot', githubCopilotProvider],
|
|
49
|
+
['gladius', gladiusProvider],
|
|
50
|
+
]);
|
|
51
|
+
// ─── Gateway ───────────────────────────────────────────────────────────────
|
|
52
|
+
export class Mach6Gateway {
|
|
53
|
+
config;
|
|
54
|
+
gatewayConfig;
|
|
55
|
+
channelRegistry;
|
|
56
|
+
toolRegistry;
|
|
57
|
+
sessionManager;
|
|
58
|
+
activeTurns = new Map();
|
|
59
|
+
provider;
|
|
60
|
+
providerName;
|
|
61
|
+
model;
|
|
62
|
+
systemPrompt;
|
|
63
|
+
shutdownRequested = false;
|
|
64
|
+
heartbeat;
|
|
65
|
+
subAgentManager;
|
|
66
|
+
startTime = Date.now();
|
|
67
|
+
httpApi = null;
|
|
68
|
+
mcpBridges = [];
|
|
69
|
+
constructor(gatewayConfig) {
|
|
70
|
+
this.gatewayConfig = gatewayConfig;
|
|
71
|
+
this.config = loadConfig(gatewayConfig.configPath);
|
|
72
|
+
// Set cwd to workspace so tools resolve relative paths correctly
|
|
73
|
+
if (this.config.workspace) {
|
|
74
|
+
process.chdir(this.config.workspace);
|
|
75
|
+
process.env.MACH6_WORKSPACE = this.config.workspace;
|
|
76
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Working directory: ${palette.cyan}${this.config.workspace}${palette.reset}`);
|
|
77
|
+
}
|
|
78
|
+
// Provider
|
|
79
|
+
this.providerName = this.config.defaultProvider;
|
|
80
|
+
this.provider = PROVIDERS.get(this.providerName);
|
|
81
|
+
if (!this.provider) {
|
|
82
|
+
throw new Error(`Unknown provider: ${this.providerName}`);
|
|
83
|
+
}
|
|
84
|
+
this.model = this.config.defaultModel;
|
|
85
|
+
// Tools
|
|
86
|
+
this.toolRegistry = new ToolRegistry();
|
|
87
|
+
for (const tool of [
|
|
88
|
+
readTool, writeTool, editTool, execTool, imageTool,
|
|
89
|
+
processStartTool, processPollTool, processKillTool, processListTool,
|
|
90
|
+
ttsTool, webFetchTool, memorySearchTool, combRecallTool, combStageTool,
|
|
91
|
+
]) {
|
|
92
|
+
this.toolRegistry.register(tool);
|
|
93
|
+
}
|
|
94
|
+
// Heartbeat scheduler
|
|
95
|
+
const hbConfig = this.gatewayConfig.heartbeat ?? {};
|
|
96
|
+
this.heartbeat = new HeartbeatScheduler({
|
|
97
|
+
activeIntervalMin: hbConfig.activeIntervalMin ?? 30,
|
|
98
|
+
idleIntervalMin: hbConfig.idleIntervalMin ?? 120,
|
|
99
|
+
sleepingIntervalMin: hbConfig.sleepingIntervalMin ?? 360,
|
|
100
|
+
quietHoursStart: hbConfig.quietHoursStart ?? 23,
|
|
101
|
+
quietHoursEnd: hbConfig.quietHoursEnd ?? 8,
|
|
102
|
+
});
|
|
103
|
+
// Sessions
|
|
104
|
+
this.sessionManager = new SessionManager(this.config.sessionsDir);
|
|
105
|
+
// System prompt (base — rebuilt per-message with channel context)
|
|
106
|
+
this.systemPrompt = buildSystemPrompt({
|
|
107
|
+
workspace: this.config.workspace,
|
|
108
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
109
|
+
});
|
|
110
|
+
console.log(`${palette.dim} [gateway]${palette.reset} System prompt: ${palette.silver}${this.systemPrompt.length} chars${palette.reset}`);
|
|
111
|
+
// Channel registry
|
|
112
|
+
this.channelRegistry = new ChannelRegistry({
|
|
113
|
+
globalOwnerIds: gatewayConfig.ownerIds,
|
|
114
|
+
onAdapterHealthChange: (id, health) => {
|
|
115
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Adapter ${palette.cyan}${id}${palette.reset}: ${palette.silver}${health.state}${palette.reset}${health.lastError ? ` ${palette.dim}(${health.lastError})${palette.reset}` : ''}`);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
// Register message tool (needs channelRegistry — must come after registry creation)
|
|
119
|
+
this.toolRegistry.register(createMessageTool(this.channelRegistry));
|
|
120
|
+
this.toolRegistry.register(createTypingTool(this.channelRegistry));
|
|
121
|
+
this.toolRegistry.register(createPresenceTool(this.channelRegistry));
|
|
122
|
+
this.toolRegistry.register(createDeleteMessageTool(this.channelRegistry));
|
|
123
|
+
this.toolRegistry.register(createMarkReadTool(this.channelRegistry));
|
|
124
|
+
// Sub-agent manager + spawn tools
|
|
125
|
+
this.subAgentManager = new SubAgentManager(this.sessionManager);
|
|
126
|
+
const provCfg = this.config.providers[this.providerName] ?? {};
|
|
127
|
+
const spawnProvConfig = {
|
|
128
|
+
model: this.model,
|
|
129
|
+
maxTokens: this.config.maxTokens,
|
|
130
|
+
temperature: this.config.temperature,
|
|
131
|
+
...provCfg,
|
|
132
|
+
};
|
|
133
|
+
this.toolRegistry.register(createSpawnTool(this.subAgentManager, this.provider, spawnProvConfig, this.toolRegistry, this.config.workspace));
|
|
134
|
+
this.toolRegistry.register(createSubAgentStatusTool(this.subAgentManager));
|
|
135
|
+
// Rebuild system prompt now that all tools are registered
|
|
136
|
+
this.systemPrompt = buildSystemPrompt({
|
|
137
|
+
workspace: this.config.workspace,
|
|
138
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// ── Start ──────────────────────────────────────────────────────────────
|
|
142
|
+
async start() {
|
|
143
|
+
console.log(versionBanner('1.0.0'));
|
|
144
|
+
const gatewayTitle = gradient('GATEWAY', [138, 43, 226], [0, 229, 255]);
|
|
145
|
+
console.log(` ${palette.bold}${gatewayTitle}${palette.reset}`);
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(kvLine('Provider', `${palette.cyan}${this.providerName}${palette.reset}${palette.dim}/${palette.reset}${palette.white}${this.model}${palette.reset}`));
|
|
148
|
+
console.log(kvLine('Tools', `${palette.gold}${this.toolRegistry.list().length}${palette.reset} ${palette.dim}registered${palette.reset}`));
|
|
149
|
+
console.log(kvLine('Workspace', `${palette.cyan}${this.config.workspace}${palette.reset}`));
|
|
150
|
+
console.log(kvLine('PID', `${palette.dim}${process.pid}${palette.reset}`));
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(divider());
|
|
153
|
+
// Connect MCP servers (external tool sources)
|
|
154
|
+
await this.connectMcpServers();
|
|
155
|
+
// Register signal handlers
|
|
156
|
+
this.setupSignals();
|
|
157
|
+
// Subscribe to bus messages
|
|
158
|
+
const bus = this.channelRegistry.getBus();
|
|
159
|
+
this.startMessageLoop();
|
|
160
|
+
// Start channels
|
|
161
|
+
await this.startChannels();
|
|
162
|
+
// Start HTTP API server
|
|
163
|
+
await this.startHttpApi();
|
|
164
|
+
const elapsed = Date.now() - this.startTime;
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(divider());
|
|
167
|
+
const readyMsg = gradient('GATEWAY READY', [0, 230, 118], [0, 188, 212]);
|
|
168
|
+
console.log(` ${palette.bold}${palette.green}⚡${palette.reset} ${palette.bold}${readyMsg}${palette.reset} ${palette.dim}— ${elapsed}ms${palette.reset}`);
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
// ── MCP Servers ────────────────────────────────────────────────────────
|
|
172
|
+
async connectMcpServers() {
|
|
173
|
+
const mcpConfig = this.gatewayConfig.mcpServers;
|
|
174
|
+
if (!mcpConfig || typeof mcpConfig !== 'object') {
|
|
175
|
+
console.log(info(`MCP: no servers configured`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const entries = Object.entries(mcpConfig);
|
|
179
|
+
const enabled = entries.filter(([_, cfg]) => cfg.enabled !== false);
|
|
180
|
+
if (enabled.length === 0) {
|
|
181
|
+
console.log(info(`MCP: no enabled servers`));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log(info(`MCP: connecting to ${palette.white}${enabled.length}${palette.reset} server(s)...`));
|
|
185
|
+
for (const [name, cfg] of enabled) {
|
|
186
|
+
try {
|
|
187
|
+
const command = [...(cfg.command ?? []), ...(cfg.args ?? [])];
|
|
188
|
+
const bridge = new McpBridge({
|
|
189
|
+
command,
|
|
190
|
+
cwd: cfg.cwd ?? this.config.workspace,
|
|
191
|
+
env: cfg.env,
|
|
192
|
+
timeout: 30000,
|
|
193
|
+
});
|
|
194
|
+
await bridge.connect();
|
|
195
|
+
// Register all discovered tools into Mach6's registry
|
|
196
|
+
const tools = bridge.getTools();
|
|
197
|
+
for (const tool of tools) {
|
|
198
|
+
this.toolRegistry.register(tool);
|
|
199
|
+
}
|
|
200
|
+
this.mcpBridges.push(bridge);
|
|
201
|
+
console.log(ok(`MCP: ${palette.cyan}${name}${palette.reset} — ${tools.length} tools`));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.log(warn(`MCP: ${name} — ${err instanceof Error ? err.message : err}`));
|
|
205
|
+
// Non-fatal — other servers + builtins still work
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Rebuild system prompt with new tools
|
|
209
|
+
if (this.mcpBridges.length > 0) {
|
|
210
|
+
this.systemPrompt = buildSystemPrompt({
|
|
211
|
+
workspace: this.config.workspace,
|
|
212
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
213
|
+
});
|
|
214
|
+
console.log(ok(`MCP: system prompt rebuilt (${palette.gold}${this.toolRegistry.list().length}${palette.reset} total tools)`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── Channel Setup ──────────────────────────────────────────────────────
|
|
218
|
+
async startChannels() {
|
|
219
|
+
const channels = this.gatewayConfig.channels;
|
|
220
|
+
if (!channels)
|
|
221
|
+
return;
|
|
222
|
+
// Discord (non-fatal — if Discord fails, other adapters still start)
|
|
223
|
+
if (channels.discord?.enabled) {
|
|
224
|
+
try {
|
|
225
|
+
console.log(info('Starting Discord adapter...'));
|
|
226
|
+
const adapter = new DiscordAdapter('discord-main');
|
|
227
|
+
const policy = {
|
|
228
|
+
dmPolicy: 'open',
|
|
229
|
+
groupPolicy: 'mention-only',
|
|
230
|
+
ownerIds: this.gatewayConfig.ownerIds ?? [],
|
|
231
|
+
requireMention: true,
|
|
232
|
+
selfId: channels.discord.botId, // Required for mention detection
|
|
233
|
+
siblingBotIds: channels.discord.siblingBotIds ?? [],
|
|
234
|
+
...channels.discord.policy,
|
|
235
|
+
};
|
|
236
|
+
await this.channelRegistry.register(adapter, { token: channels.discord.token, botId: channels.discord.botId }, policy);
|
|
237
|
+
console.log(ok(`Discord ${palette.green}connected${palette.reset}`));
|
|
238
|
+
presenceManager.registerAdapter('discord-main', (chatId) => adapter.typing(chatId));
|
|
239
|
+
// Register Discord client for rich activity presence
|
|
240
|
+
const discordClient = adapter.getClient();
|
|
241
|
+
if (discordClient)
|
|
242
|
+
presenceManager.registerDiscordClient('discord-main', discordClient);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.log(warn(`Discord failed — ${err.message}`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// WhatsApp (non-fatal — log and continue if it fails)
|
|
249
|
+
if (channels.whatsapp?.enabled) {
|
|
250
|
+
try {
|
|
251
|
+
console.log(info('Starting WhatsApp adapter...'));
|
|
252
|
+
const adapter = new WhatsAppAdapter('whatsapp-main');
|
|
253
|
+
const policy = {
|
|
254
|
+
dmPolicy: 'allowlist',
|
|
255
|
+
groupPolicy: 'mention-only',
|
|
256
|
+
ownerIds: this.gatewayConfig.ownerIds ?? [],
|
|
257
|
+
allowedSenders: this.gatewayConfig.ownerIds ?? [],
|
|
258
|
+
...channels.whatsapp.policy,
|
|
259
|
+
};
|
|
260
|
+
await this.channelRegistry.register(adapter, {
|
|
261
|
+
authDir: channels.whatsapp.authDir,
|
|
262
|
+
phoneNumber: channels.whatsapp.phoneNumber,
|
|
263
|
+
autoRead: channels.whatsapp.autoRead ?? true,
|
|
264
|
+
onQR: (qr) => {
|
|
265
|
+
console.log(`\n ${palette.gold}📱 WhatsApp QR Code — scan to link:${palette.reset}\n${qr}\n`);
|
|
266
|
+
},
|
|
267
|
+
}, policy);
|
|
268
|
+
console.log(ok(`WhatsApp ${palette.green}connected${palette.reset}`));
|
|
269
|
+
presenceManager.registerAdapter('whatsapp-main', (chatId) => adapter.typing(chatId));
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.log(warn(`WhatsApp failed — ${err.message}`));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ── HTTP API ───────────────────────────────────────────────────────────
|
|
277
|
+
async startHttpApi() {
|
|
278
|
+
const port = this.gatewayConfig.apiPort ?? 3006;
|
|
279
|
+
const apiKey = process.env.MACH6_API_KEY || process.env.API_KEY || '';
|
|
280
|
+
if (!apiKey) {
|
|
281
|
+
console.log(warn('No MACH6_API_KEY — HTTP API disabled'));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.httpApi = new HttpApiServer({
|
|
285
|
+
port,
|
|
286
|
+
apiKey,
|
|
287
|
+
allowedOrigins: this.config.allowedOrigins ?? ['*'],
|
|
288
|
+
onChat: async (request) => {
|
|
289
|
+
return this.handleHttpChat(request);
|
|
290
|
+
},
|
|
291
|
+
onRelay: async (target, text) => {
|
|
292
|
+
// Relay to WhatsApp
|
|
293
|
+
try {
|
|
294
|
+
const result = await this.channelRegistry.sendToChannel('whatsapp', target, {
|
|
295
|
+
content: text,
|
|
296
|
+
});
|
|
297
|
+
return { success: result.success, error: result.error };
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
onHealth: () => this.status(),
|
|
304
|
+
});
|
|
305
|
+
await this.httpApi.start();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Handle an HTTP API chat request by running it through the full agent pipeline.
|
|
309
|
+
* Creates a synthetic BusEnvelope and processes it like any other channel message.
|
|
310
|
+
*/
|
|
311
|
+
handleHttpChat(request) {
|
|
312
|
+
return new Promise(async (resolve, reject) => {
|
|
313
|
+
const sessionId = request.sessionId ?? `http-${request.source ?? 'web'}-${request.senderId ?? 'anon'}`;
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
try {
|
|
316
|
+
// Load or create session
|
|
317
|
+
let session = this.sessionManager.load(sessionId) ?? this.sessionManager.create(sessionId, {
|
|
318
|
+
provider: this.providerName,
|
|
319
|
+
model: this.model,
|
|
320
|
+
});
|
|
321
|
+
// Build system prompt
|
|
322
|
+
const turnPrompt = buildSystemPrompt({
|
|
323
|
+
workspace: this.config.workspace,
|
|
324
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
325
|
+
channel: 'http',
|
|
326
|
+
chatType: 'direct',
|
|
327
|
+
senderId: request.senderId ?? 'http-user',
|
|
328
|
+
});
|
|
329
|
+
if (session.messages.length > 0 && session.messages[0].role === 'system') {
|
|
330
|
+
session.messages[0].content = turnPrompt;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
session.messages.unshift({ role: 'system', content: turnPrompt });
|
|
334
|
+
}
|
|
335
|
+
// Build user message content
|
|
336
|
+
const userContent = request.senderName
|
|
337
|
+
? `[${request.senderName}] ${request.text}`
|
|
338
|
+
: request.text;
|
|
339
|
+
session.messages.push({ role: 'user', content: userContent });
|
|
340
|
+
// Sandbox context — HTTP API users get 'standard' tier (not admin)
|
|
341
|
+
const ownerIds = this.gatewayConfig.ownerIds ?? [];
|
|
342
|
+
const isOwner = request.senderId ? ownerIds.includes(request.senderId) : false;
|
|
343
|
+
const sandboxCtx = {
|
|
344
|
+
sessionId,
|
|
345
|
+
adapterId: 'http-api',
|
|
346
|
+
channelType: 'http',
|
|
347
|
+
chatType: 'direct',
|
|
348
|
+
senderId: request.senderId ?? 'http-user',
|
|
349
|
+
isOwner,
|
|
350
|
+
};
|
|
351
|
+
const sandboxedTools = createSandboxedRegistry(this.toolRegistry, sandboxCtx);
|
|
352
|
+
// Provider config
|
|
353
|
+
const providerCfg = this.config.providers[this.providerName] ?? {};
|
|
354
|
+
const provConfig = {
|
|
355
|
+
model: this.model,
|
|
356
|
+
maxTokens: this.config.maxTokens,
|
|
357
|
+
temperature: this.config.temperature,
|
|
358
|
+
systemPrompt: this.systemPrompt,
|
|
359
|
+
...providerCfg,
|
|
360
|
+
};
|
|
361
|
+
// Run agent
|
|
362
|
+
console.log(`${palette.dim} [http]${palette.reset} Agent turn for ${palette.violet}${sessionId}${palette.reset}`);
|
|
363
|
+
const startMs = Date.now();
|
|
364
|
+
const result = await runAgent(session.messages, {
|
|
365
|
+
provider: this.provider,
|
|
366
|
+
providerConfig: provConfig,
|
|
367
|
+
toolRegistry: sandboxedTools,
|
|
368
|
+
sessionId,
|
|
369
|
+
maxIterations: this.config.maxIterations ?? 50,
|
|
370
|
+
abortSignal: controller.signal,
|
|
371
|
+
onEvent: (ev) => {
|
|
372
|
+
if (ev.type === 'usage') {
|
|
373
|
+
this.sessionManager.trackUsage(session, ev.usage.inputTokens, ev.usage.outputTokens);
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
onToolStart: (name) => console.log(` ${palette.violet}⚡ ${name}${palette.reset}`),
|
|
377
|
+
onToolEnd: (name) => console.log(` ${palette.green}✓ ${name}${palette.reset}`),
|
|
378
|
+
});
|
|
379
|
+
// Save session
|
|
380
|
+
session.messages = result.messages;
|
|
381
|
+
if (result.text) {
|
|
382
|
+
session.messages.push({ role: 'assistant', content: result.text });
|
|
383
|
+
}
|
|
384
|
+
this.sessionManager.save(session);
|
|
385
|
+
resolve({
|
|
386
|
+
text: result.text ?? '',
|
|
387
|
+
sessionId,
|
|
388
|
+
durationMs: Date.now() - startMs,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
reject(err);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// ── Message Loop ───────────────────────────────────────────────────────
|
|
397
|
+
/**
|
|
398
|
+
* The core message loop. Polls the bus for new messages and dispatches
|
|
399
|
+
* agent turns. Each session gets at most one concurrent agent turn.
|
|
400
|
+
*
|
|
401
|
+
* When a new message arrives during an active turn, the bus handles it:
|
|
402
|
+
* - interrupt priority → cancels current turn
|
|
403
|
+
* - high priority → queued, injected at next iteration
|
|
404
|
+
* - normal/low → queued for after current turn
|
|
405
|
+
*/
|
|
406
|
+
startMessageLoop() {
|
|
407
|
+
const bus = this.channelRegistry.getBus();
|
|
408
|
+
const knownSessions = new Set();
|
|
409
|
+
// Check for new sessions periodically
|
|
410
|
+
setInterval(() => {
|
|
411
|
+
if (this.shutdownRequested)
|
|
412
|
+
return;
|
|
413
|
+
const routes = this.channelRegistry.getRouter().getRoutes();
|
|
414
|
+
for (const route of routes) {
|
|
415
|
+
if (knownSessions.has(route.sessionId))
|
|
416
|
+
continue;
|
|
417
|
+
knownSessions.add(route.sessionId);
|
|
418
|
+
// Subscribe to this session
|
|
419
|
+
bus.subscribe(route.sessionId, (envelope) => {
|
|
420
|
+
this.handleEnvelope(envelope);
|
|
421
|
+
});
|
|
422
|
+
// Subscribe to interrupts
|
|
423
|
+
bus.onInterrupt(route.sessionId, (envelope) => {
|
|
424
|
+
this.handleInterrupt(envelope);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}, 100);
|
|
428
|
+
}
|
|
429
|
+
pendingEnvelopes = new Map();
|
|
430
|
+
async handleEnvelope(envelope) {
|
|
431
|
+
const sessionId = envelope.sessionId;
|
|
432
|
+
// Check if there's already an active turn for this session
|
|
433
|
+
if (this.activeTurns.has(sessionId)) {
|
|
434
|
+
// Queue the envelope for when the current turn finishes
|
|
435
|
+
const pending = this.pendingEnvelopes.get(sessionId) ?? [];
|
|
436
|
+
pending.push(envelope);
|
|
437
|
+
this.pendingEnvelopes.set(sessionId, pending);
|
|
438
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Queued for active session ${palette.violet}${sessionId}${palette.reset} ${palette.dim}(${pending.length} pending)${palette.reset}`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// Start a new agent turn
|
|
442
|
+
await this.runAgentTurn(envelope);
|
|
443
|
+
}
|
|
444
|
+
handleInterrupt(envelope) {
|
|
445
|
+
const sessionId = envelope.sessionId;
|
|
446
|
+
const active = this.activeTurns.get(sessionId);
|
|
447
|
+
if (!active)
|
|
448
|
+
return;
|
|
449
|
+
console.log(`${palette.dim} [gateway]${palette.reset} ${palette.yellow}Interrupting${palette.reset} session ${palette.violet}${sessionId}${palette.reset}`);
|
|
450
|
+
active.abortController.abort('new_message');
|
|
451
|
+
}
|
|
452
|
+
// ── Agent Turn ─────────────────────────────────────────────────────────
|
|
453
|
+
async runAgentTurn(envelope) {
|
|
454
|
+
const sessionId = envelope.sessionId;
|
|
455
|
+
const controller = new AbortController();
|
|
456
|
+
const turn = {
|
|
457
|
+
sessionId,
|
|
458
|
+
abortController: controller,
|
|
459
|
+
startedAt: Date.now(),
|
|
460
|
+
channelType: envelope.source.channelType,
|
|
461
|
+
chatId: envelope.source.chatId,
|
|
462
|
+
adapterId: envelope.source.adapterId,
|
|
463
|
+
};
|
|
464
|
+
this.activeTurns.set(sessionId, turn);
|
|
465
|
+
// Build sandbox context for this session
|
|
466
|
+
const ownerIds = this.gatewayConfig.ownerIds ?? [];
|
|
467
|
+
const isOwner = ownerIds.includes(envelope.source.senderId);
|
|
468
|
+
const chatType = (envelope.source.chatId.includes('@g.') || envelope.metadata.guildId) ? 'group' : 'direct';
|
|
469
|
+
const sandboxCtx = {
|
|
470
|
+
sessionId,
|
|
471
|
+
adapterId: envelope.source.adapterId,
|
|
472
|
+
channelType: envelope.source.channelType,
|
|
473
|
+
chatType,
|
|
474
|
+
senderId: envelope.source.senderId,
|
|
475
|
+
isOwner,
|
|
476
|
+
};
|
|
477
|
+
const sandboxedTools = createSandboxedRegistry(this.toolRegistry, sandboxCtx);
|
|
478
|
+
// Record user activity for heartbeat scheduling
|
|
479
|
+
if (envelope.source.adapterId !== 'heartbeat') {
|
|
480
|
+
this.heartbeat.recordUserActivity();
|
|
481
|
+
}
|
|
482
|
+
this.channelRegistry.setSessionActive(sessionId, true);
|
|
483
|
+
// Start sustained typing (refreshes every 4s until response sent)
|
|
484
|
+
const typingTarget = { adapterId: envelope.source.adapterId, chatId: envelope.source.chatId };
|
|
485
|
+
presenceManager.startTyping(typingTarget);
|
|
486
|
+
try {
|
|
487
|
+
// Load or create session
|
|
488
|
+
let session = this.sessionManager.load(sessionId) ?? this.sessionManager.create(sessionId, {
|
|
489
|
+
provider: this.providerName,
|
|
490
|
+
model: this.model,
|
|
491
|
+
});
|
|
492
|
+
// Build channel-aware system prompt (refreshes workspace files each turn)
|
|
493
|
+
const turnPrompt = buildSystemPrompt({
|
|
494
|
+
workspace: this.config.workspace,
|
|
495
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
496
|
+
channel: envelope.source.channelType,
|
|
497
|
+
chatType: envelope.source.chatId.includes('@g.') ? 'group' : 'direct',
|
|
498
|
+
senderId: envelope.source.senderId,
|
|
499
|
+
});
|
|
500
|
+
// Replace or insert system prompt (always fresh — workspace files may have changed)
|
|
501
|
+
if (session.messages.length > 0 && session.messages[0].role === 'system') {
|
|
502
|
+
session.messages[0].content = turnPrompt;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
session.messages.unshift({ role: 'system', content: turnPrompt });
|
|
506
|
+
}
|
|
507
|
+
// Add user message
|
|
508
|
+
const userContent = this.buildUserContent(envelope);
|
|
509
|
+
session.messages.push({ role: 'user', content: userContent });
|
|
510
|
+
// Pre-flight context trim: estimate token count and archive if approaching limit
|
|
511
|
+
// Rough estimate: 1 token ≈ 4 chars. Model limit 128K, leave 20K headroom.
|
|
512
|
+
const TOKEN_LIMIT = 128_000;
|
|
513
|
+
const HEADROOM = 20_000;
|
|
514
|
+
const estimatedTokens = session.messages.reduce((sum, m) => {
|
|
515
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
516
|
+
return sum + Math.ceil(content.length / 4);
|
|
517
|
+
}, 0);
|
|
518
|
+
if (estimatedTokens > TOKEN_LIMIT - HEADROOM) {
|
|
519
|
+
console.log(`${palette.dim} [gateway]${palette.reset} ${palette.yellow}⚠${palette.reset} Pre-flight trim: ~${estimatedTokens} tokens ${palette.dim}(limit ${TOKEN_LIMIT})${palette.reset}`);
|
|
520
|
+
const archived = this.sessionManager.archive(sessionId, 30);
|
|
521
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Archived ${archived} messages → ${session.messages.length} remaining`);
|
|
522
|
+
// Reload session after archive
|
|
523
|
+
const trimmed = this.sessionManager.load(sessionId);
|
|
524
|
+
if (trimmed) {
|
|
525
|
+
session.messages = trimmed.messages;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Provider config
|
|
529
|
+
const providerCfg = this.config.providers[this.providerName] ?? {};
|
|
530
|
+
const provConfig = {
|
|
531
|
+
model: this.model,
|
|
532
|
+
maxTokens: this.config.maxTokens,
|
|
533
|
+
temperature: this.config.temperature,
|
|
534
|
+
systemPrompt: this.systemPrompt,
|
|
535
|
+
...providerCfg,
|
|
536
|
+
};
|
|
537
|
+
// Run agent
|
|
538
|
+
console.log(`\n${palette.dim} [turn]${palette.reset} ${palette.violet}${sessionId}${palette.reset} ${palette.dim}via${palette.reset} ${envelope.source.channelType}${palette.dim}/${palette.reset}${envelope.source.chatId}`);
|
|
539
|
+
const turnStartTime = Date.now();
|
|
540
|
+
const result = await runAgent(session.messages, {
|
|
541
|
+
provider: this.provider,
|
|
542
|
+
providerConfig: provConfig,
|
|
543
|
+
toolRegistry: sandboxedTools,
|
|
544
|
+
sessionId,
|
|
545
|
+
maxIterations: this.config.maxIterations ?? 50,
|
|
546
|
+
abortSignal: controller.signal,
|
|
547
|
+
onEvent: (ev) => {
|
|
548
|
+
if (ev.type === 'usage') {
|
|
549
|
+
this.sessionManager.trackUsage(session, ev.usage.inputTokens, ev.usage.outputTokens);
|
|
550
|
+
console.log(` ${palette.dim}📊 +${ev.usage.inputTokens}in / +${ev.usage.outputTokens}out${palette.reset}`);
|
|
551
|
+
}
|
|
552
|
+
// Update presence when LLM starts streaming
|
|
553
|
+
if (ev.type === 'text_delta' || ev.type === 'done') {
|
|
554
|
+
presenceManager.llmStreaming();
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
onToolStart: (name) => {
|
|
558
|
+
console.log(` ${palette.violet}⚡ ${name}${palette.reset}`);
|
|
559
|
+
presenceManager.toolStart(name);
|
|
560
|
+
},
|
|
561
|
+
onToolEnd: (name, res) => {
|
|
562
|
+
const preview = res.length > 100 ? res.slice(0, 100) + '...' : res;
|
|
563
|
+
console.log(` ${palette.green}✓ ${name}${palette.reset} ${palette.dim}${preview.split('\n')[0]}${palette.reset}`);
|
|
564
|
+
presenceManager.toolEnd(name);
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
const turnElapsed = Date.now() - turnStartTime;
|
|
568
|
+
console.log(`${palette.dim} [turn]${palette.reset} Complete ${palette.dim}— ${turnElapsed}ms, ${result.iterations} iter, ${result.toolCalls.length} tools${palette.reset}`);
|
|
569
|
+
// Save session
|
|
570
|
+
session.messages = result.messages;
|
|
571
|
+
if (result.text) {
|
|
572
|
+
session.messages.push({ role: 'assistant', content: result.text });
|
|
573
|
+
}
|
|
574
|
+
this.sessionManager.save(session);
|
|
575
|
+
// Auto-archive bloated sessions (>200KB → keep last 30 messages)
|
|
576
|
+
this.sessionManager.autoArchive();
|
|
577
|
+
// Send response back through the channel
|
|
578
|
+
if (result.text && result.text !== 'NO_REPLY' && result.text !== 'HEARTBEAT_OK') {
|
|
579
|
+
console.log(`${palette.dim} [send]${palette.reset} → ${envelope.source.adapterId}/${envelope.source.chatId} ${palette.dim}(${result.text.length} chars)${palette.reset}`);
|
|
580
|
+
try {
|
|
581
|
+
const sendResult = await this.channelRegistry.send(envelope.source.adapterId, envelope.source.chatId, {
|
|
582
|
+
content: result.text,
|
|
583
|
+
replyToId: envelope.metadata.platformMessageId,
|
|
584
|
+
});
|
|
585
|
+
console.log(`${palette.dim} [send]${palette.reset} ${palette.green}delivered${palette.reset}`);
|
|
586
|
+
}
|
|
587
|
+
catch (sendErr) {
|
|
588
|
+
console.error(` ${palette.red}✗ [send]${palette.reset} ${sendErr}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
console.log(`${palette.dim} [turn]${palette.reset} No response ${palette.dim}(${result.text ? result.text.slice(0, 50) : 'null'})${palette.reset}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
if (controller.signal.aborted) {
|
|
597
|
+
console.log(`${palette.dim} [turn]${palette.reset} ${palette.yellow}Interrupted${palette.reset} ${palette.violet}${sessionId}${palette.reset}`);
|
|
598
|
+
// Re-process with accumulated messages
|
|
599
|
+
// Check our pending queue + bus drain
|
|
600
|
+
const pending = this.pendingEnvelopes.get(sessionId) ?? [];
|
|
601
|
+
const busPending = this.channelRegistry.getBus().drain(sessionId);
|
|
602
|
+
const allPending = [...pending, ...busPending];
|
|
603
|
+
this.pendingEnvelopes.delete(sessionId);
|
|
604
|
+
if (allPending.length > 0) {
|
|
605
|
+
// Recursion with new context
|
|
606
|
+
this.activeTurns.delete(sessionId);
|
|
607
|
+
this.channelRegistry.setSessionActive(sessionId, false);
|
|
608
|
+
await this.runAgentTurn(allPending[allPending.length - 1]); // most recent message
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
console.error(` ${palette.red}✗ [turn]${palette.reset} ${palette.violet}${sessionId}${palette.reset}: ${err}`);
|
|
614
|
+
// Send error message
|
|
615
|
+
try {
|
|
616
|
+
await this.channelRegistry.send(envelope.source.adapterId, envelope.source.chatId, { content: `⚠️ Error: ${err instanceof Error ? err.message : String(err)}` });
|
|
617
|
+
}
|
|
618
|
+
catch { /* ignore send errors */ }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
this.activeTurns.delete(sessionId);
|
|
623
|
+
this.channelRegistry.setSessionActive(sessionId, false);
|
|
624
|
+
presenceManager.stopTyping(typingTarget);
|
|
625
|
+
// Process any pending messages that arrived during this turn
|
|
626
|
+
const pending = this.pendingEnvelopes.get(sessionId);
|
|
627
|
+
if (pending && pending.length > 0) {
|
|
628
|
+
this.pendingEnvelopes.delete(sessionId);
|
|
629
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Processing ${pending.length} pending for ${palette.violet}${sessionId}${palette.reset}`);
|
|
630
|
+
// Process the most recent pending message (others are stale context)
|
|
631
|
+
await this.runAgentTurn(pending[pending.length - 1]);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
buildUserContent(envelope) {
|
|
636
|
+
const parts = [];
|
|
637
|
+
// Sender context
|
|
638
|
+
if (envelope.source.senderName) {
|
|
639
|
+
parts.push(`[${envelope.source.senderName}]`);
|
|
640
|
+
}
|
|
641
|
+
// Reply context
|
|
642
|
+
if (envelope.source.replyToId) {
|
|
643
|
+
parts.push(`(replying to message ${envelope.source.replyToId})`);
|
|
644
|
+
}
|
|
645
|
+
// Text
|
|
646
|
+
if (envelope.payload.text) {
|
|
647
|
+
parts.push(envelope.payload.text);
|
|
648
|
+
}
|
|
649
|
+
// Media descriptions — include local path if downloaded
|
|
650
|
+
if (envelope.payload.media?.length) {
|
|
651
|
+
for (const m of envelope.payload.media) {
|
|
652
|
+
const desc = [m.type];
|
|
653
|
+
if (m.filename)
|
|
654
|
+
desc.push(m.filename);
|
|
655
|
+
else if (m.mimeType)
|
|
656
|
+
desc.push(m.mimeType);
|
|
657
|
+
if (m.path)
|
|
658
|
+
desc.push(`path=${m.path}`);
|
|
659
|
+
if (m.caption)
|
|
660
|
+
desc.push(`caption="${m.caption}"`);
|
|
661
|
+
if (m.width && m.height)
|
|
662
|
+
desc.push(`${m.width}x${m.height}`);
|
|
663
|
+
parts.push(`[${desc.join(', ')}]`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return parts.join(' ');
|
|
667
|
+
}
|
|
668
|
+
// ── Signals ────────────────────────────────────────────────────────────
|
|
669
|
+
setupSignals() {
|
|
670
|
+
const shutdown = async (signal) => {
|
|
671
|
+
if (this.shutdownRequested)
|
|
672
|
+
return;
|
|
673
|
+
this.shutdownRequested = true;
|
|
674
|
+
console.log(`\n${palette.dim} [gateway]${palette.reset} ${palette.yellow}${signal}${palette.reset} — shutting down...`);
|
|
675
|
+
// Cancel all active turns
|
|
676
|
+
for (const [, turn] of this.activeTurns) {
|
|
677
|
+
turn.abortController.abort('shutdown');
|
|
678
|
+
}
|
|
679
|
+
// Disconnect all channels
|
|
680
|
+
await this.channelRegistry.destroy();
|
|
681
|
+
// Stop HTTP API
|
|
682
|
+
if (this.httpApi)
|
|
683
|
+
await this.httpApi.stop();
|
|
684
|
+
// Disconnect MCP bridges
|
|
685
|
+
for (const bridge of this.mcpBridges) {
|
|
686
|
+
try {
|
|
687
|
+
bridge.disconnect();
|
|
688
|
+
}
|
|
689
|
+
catch { /* ignore */ }
|
|
690
|
+
}
|
|
691
|
+
presenceManager.stopAll();
|
|
692
|
+
this.heartbeat.stop();
|
|
693
|
+
console.log(`${palette.dim} [gateway]${palette.reset} Shutdown complete.`);
|
|
694
|
+
process.exit(0);
|
|
695
|
+
};
|
|
696
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
697
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
698
|
+
// SIGUSR1 = hot-reload config (Linux/macOS only — not supported on Windows)
|
|
699
|
+
// On Windows: restart the process, or POST /api/v1/health to verify state
|
|
700
|
+
if (process.platform !== 'win32') {
|
|
701
|
+
process.on('SIGUSR1', () => {
|
|
702
|
+
console.log(`${palette.dim} [gateway]${palette.reset} ${palette.cyan}SIGUSR1${palette.reset} — reloading config...`);
|
|
703
|
+
try {
|
|
704
|
+
this.config = loadConfig(this.gatewayConfig.configPath);
|
|
705
|
+
this.providerName = this.config.defaultProvider;
|
|
706
|
+
this.provider = PROVIDERS.get(this.providerName);
|
|
707
|
+
this.model = this.config.defaultModel;
|
|
708
|
+
this.systemPrompt = buildSystemPrompt({
|
|
709
|
+
workspace: this.config.workspace,
|
|
710
|
+
tools: this.toolRegistry.list().map(t => t.name),
|
|
711
|
+
});
|
|
712
|
+
console.log(`${palette.dim} [gateway]${palette.reset} System prompt refreshed ${palette.dim}(${this.systemPrompt.length} chars)${palette.reset}`);
|
|
713
|
+
console.log(ok('Config reloaded successfully'));
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
console.error(` ${palette.red}✗ [gateway]${palette.reset} Config reload failed: ${err}`);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// ── Status ─────────────────────────────────────────────────────────────
|
|
722
|
+
status() {
|
|
723
|
+
return {
|
|
724
|
+
uptime: Date.now() - this.startTime,
|
|
725
|
+
provider: `${this.providerName}/${this.model}`,
|
|
726
|
+
channels: this.channelRegistry.list(),
|
|
727
|
+
activeTurns: this.activeTurns.size,
|
|
728
|
+
sessions: this.sessionManager.list().length,
|
|
729
|
+
tools: this.toolRegistry.list().length,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// ─── CLI Entry ─────────────────────────────────────────────────────────────
|
|
734
|
+
export async function startGateway(configPath) {
|
|
735
|
+
// Load gateway config from mach6.json or env
|
|
736
|
+
const config = loadConfig(configPath);
|
|
737
|
+
// Build gateway config from environment + mach6.json
|
|
738
|
+
const gatewayConfig = {
|
|
739
|
+
configPath,
|
|
740
|
+
ownerIds: config.ownerIds ?? [],
|
|
741
|
+
channels: {
|
|
742
|
+
discord: {
|
|
743
|
+
enabled: !!process.env.DISCORD_BOT_TOKEN || !!config.discord?.token,
|
|
744
|
+
token: process.env.DISCORD_BOT_TOKEN ?? config.discord?.token ?? '',
|
|
745
|
+
botId: config.discord?.botId,
|
|
746
|
+
siblingBotIds: config.discord?.siblingBotIds,
|
|
747
|
+
policy: config.discord?.policy,
|
|
748
|
+
},
|
|
749
|
+
whatsapp: {
|
|
750
|
+
enabled: !!config.whatsapp?.enabled,
|
|
751
|
+
authDir: config.whatsapp?.authDir ?? path.join(os.homedir(), '.mach6', 'whatsapp-auth'),
|
|
752
|
+
phoneNumber: config.whatsapp?.phoneNumber,
|
|
753
|
+
autoRead: config.whatsapp?.autoRead ?? true,
|
|
754
|
+
policy: config.whatsapp?.policy,
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
apiPort: config.apiPort ?? 3006,
|
|
758
|
+
};
|
|
759
|
+
const gateway = new Mach6Gateway(gatewayConfig);
|
|
760
|
+
await gateway.start();
|
|
761
|
+
return gateway;
|
|
762
|
+
}
|
|
763
|
+
// Run directly
|
|
764
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
765
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
766
|
+
const configPath = process.argv.find(a => a.startsWith('--config='))?.split('=')[1];
|
|
767
|
+
startGateway(configPath).catch(err => {
|
|
768
|
+
console.error(` ${palette.red}✗${palette.reset} Gateway startup failed: ${err}`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
//# sourceMappingURL=daemon.js.map
|