Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/providers/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}),
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
61 changes: 46 additions & 15 deletions packages/workflows/src/dag-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down