From 43c0b971f26daab36e7f974ca1638e756516c02c Mon Sep 17 00:00:00 2001 From: MrAi Date: Sun, 12 Apr 2026 20:29:47 +0200 Subject: [PATCH] fix: Windows HOME resolution and MCP plugin warning suppression - Use getArchonHome() instead of process.env.HOME for cross-platform config path resolution in CLI and server entry points - Suppress non-fatal MCP connection failures from user-level plugins (e.g., telegram) in headless workflow subprocesses - Only surface MCP failures for workflow-configured servers Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/cli.test.ts | 3 +- packages/cli/src/cli.ts | 3 +- packages/core/src/providers/claude.ts | 4 ++ packages/server/src/index.ts | 4 +- packages/workflows/src/dag-executor.ts | 61 +++++++++++++++++++------- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts index 40b98e4887..0df30fc202 100644 --- a/packages/cli/src/cli.test.ts +++ b/packages/cli/src/cli.test.ts @@ -6,6 +6,7 @@ */ import { describe, it, expect } from 'bun:test'; import { parseArgs } from 'util'; +import { getArchonHome } from '@archon/paths'; import * as git from '@archon/git'; // Test the argument parsing logic used in cli.ts @@ -237,7 +238,7 @@ describe('CLI env isolation', () => { process.env.TEST_ARCHON_OVERRIDE = 'from-cwd-env'; // Write a temporary env content and load with override - const globalEnvPath = resolve(process.env.HOME ?? '~', '.archon', '.env'); + const globalEnvPath = resolve(getArchonHome(), '.env'); if (existsSync(globalEnvPath)) { const result = config({ path: globalEnvPath, override: true }); // If ~/.archon/.env exists and has DATABASE_URL, it should override diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d7dedf4810..4157ebaeb8 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -10,6 +10,7 @@ // Must be the very first import — strips Bun-auto-loaded CWD .env keys before // any module reads process.env at init time (e.g. @archon/paths/logger reads LOG_LEVEL). import '@archon/paths/strip-cwd-env-boot'; +import { getArchonHome } from '@archon/paths'; import { parseArgs } from 'util'; import { config } from 'dotenv'; import { resolve } from 'path'; @@ -19,7 +20,7 @@ import { existsSync } from 'fs'; // over shell-inherited env vars (e.g. PORT, LOG_LEVEL from shell profile). // CWD .env keys are already gone (stripCwdEnv above), so override only // affects shell-inherited values, which is the intended behavior. -const globalEnvPath = resolve(process.env.HOME ?? '~', '.archon', '.env'); +const globalEnvPath = resolve(getArchonHome(), '.env'); if (existsSync(globalEnvPath)) { const result = config({ path: globalEnvPath, override: true }); if (result.error) { diff --git a/packages/core/src/providers/claude.ts b/packages/core/src/providers/claude.ts index 0d8c6d4596..0e864cb210 100644 --- a/packages/core/src/providers/claude.ts +++ b/packages/core/src/providers/claude.ts @@ -385,6 +385,10 @@ export class ClaudeProvider implements IAgentProvider { allowDangerouslySkipPermissions: true, systemPrompt: requestOptions?.systemPrompt ?? { type: 'preset', preset: 'claude_code' }, settingSources: requestOptions?.settingSources ?? ['project'], + // Disable user plugins in the subprocess — Archon manages its own MCP servers + // via the mcpServers option. User plugins (e.g., telegram) can fail to connect + // in the headless subprocess and produce spurious warnings. + settings: { enabledPlugins: {} }, // Merge user-provided hooks with our PostToolUse capture hook hooks: { ...(requestOptions?.hooks ?? {}), diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0b502008d6..24420ec0a9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -13,7 +13,7 @@ import '@archon/paths/strip-cwd-env-boot'; import { config } from 'dotenv'; import { resolve } from 'path'; import { existsSync } from 'fs'; -import { BUNDLED_IS_BINARY } from '@archon/paths'; +import { BUNDLED_IS_BINARY, getArchonHome } from '@archon/paths'; // In dev/source mode, load the repo root .env (platform tokens, API keys, etc.) // import.meta.dir is frozen at build time, so skip in compiled binaries. @@ -31,7 +31,7 @@ if (envPath) { // Load ~/.archon/.env with override — Archon's config always wins over any // Bun-auto-loaded CWD vars. In binary mode this is the single source of truth. // In dev mode it overrides CWD vars for keys like DATABASE_URL. -const globalEnvPath = resolve(process.env.HOME ?? '~', '.archon', '.env'); +const globalEnvPath = resolve(getArchonHome(), '.env'); if (existsSync(globalEnvPath)) { const globalResult = config({ path: globalEnvPath, override: true }); if (globalResult.error) { diff --git a/packages/workflows/src/dag-executor.ts b/packages/workflows/src/dag-executor.ts index af86b2e055..dcdf6a8450 100644 --- a/packages/workflows/src/dag-executor.ts +++ b/packages/workflows/src/dag-executor.ts @@ -822,6 +822,11 @@ async function executeNodeInternal( const aiClient = deps.getAgentProvider(provider); const streamingMode = platform.getStreamingMode(); + // Track workflow-configured MCP server names to distinguish from user plugin MCP servers. + // User plugins (e.g., telegram) can fail to connect in headless subprocesses — those + // failures are non-fatal noise and should not be surfaced to the user. + const workflowMcpNames = new Set(Object.keys(nodeOptions?.mcpServers ?? {})); + let nodeOutputText = ''; // Always accumulate regardless of streaming mode let structuredOutput: unknown; let newSessionId: string | undefined; @@ -1026,22 +1031,48 @@ async function executeNodeInternal( } break; // Result is the "I'm done" signal — don't wait for subprocess to exit } else if (msg.type === 'system' && msg.content) { - // Surface MCP connection failures to the user + // MCP connection failures — only surface for workflow-configured servers, + // not for user plugins (e.g., telegram) that fail in headless subprocesses. if (msg.content.startsWith('MCP server connection failed:')) { - getLog().warn( - { nodeId: node.id, mcpStatus: msg.content }, - 'dag.mcp_server_connection_failed' - ); - const delivered = await safeSendMessage( - platform, - conversationId, - msg.content, - nodeContext - ); - if (!delivered) { - getLog().error( - { nodeId: node.id, mcpStatus: msg.content, workflowRunId: workflowRun.id }, - 'dag.mcp_connection_failure_delivery_failed' + // Parse failed entries: "MCP server connection failed: name1 (status1), name2 (status2)" + const failedEntries = msg.content + .slice('MCP server connection failed: '.length) + .split(', ') + .map(entry => entry.trim()) + .filter(Boolean); + const workflowFailures = failedEntries.filter(entry => { + const name = entry.split(' (')[0]; + return workflowMcpNames.has(name); + }); + + if (workflowFailures.length > 0) { + const workflowMsg = `MCP server connection failed: ${workflowFailures.join(', ')}`; + getLog().warn( + { nodeId: node.id, mcpStatus: workflowMsg }, + 'dag.mcp_server_connection_failed' + ); + const delivered = await safeSendMessage( + platform, + conversationId, + workflowMsg, + nodeContext + ); + if (!delivered) { + getLog().error( + { nodeId: node.id, mcpStatus: workflowMsg, workflowRunId: workflowRun.id }, + 'dag.mcp_connection_failure_delivery_failed' + ); + } + } + // User-plugin MCP failures: log at debug, don't surface to user + const pluginFailures = failedEntries.filter(entry => { + const name = entry.split(' (')[0]; + return !workflowMcpNames.has(name); + }); + if (pluginFailures.length > 0) { + getLog().debug( + { nodeId: node.id, mcpStatus: pluginFailures.join(', ') }, + 'dag.mcp_plugin_connection_failed' ); } } else {