refactor: complete config unification and split db helpers
This commit is contained in:
243
src/config.ts
243
src/config.ts
@@ -1,19 +1,18 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { loadConfig } from './config/load-config.js';
|
||||
import type { MoaConfig } from './moa.js';
|
||||
import type { RoleModelConfig } from './config/schema.js';
|
||||
|
||||
import type { AgentType } from './types.js';
|
||||
import { getEnv } from './env.js';
|
||||
export type { AppConfig, RoleModelConfig } from './config/schema.js';
|
||||
export { loadConfig } from './config/load-config.js';
|
||||
|
||||
export const ASSISTANT_NAME = getEnv('ASSISTANT_NAME') || 'Andy';
|
||||
export const ASSISTANT_HAS_OWN_NUMBER =
|
||||
getEnv('ASSISTANT_HAS_OWN_NUMBER') === 'true';
|
||||
const ASSISTANT_SLUG = ASSISTANT_NAME.trim().toLowerCase();
|
||||
export const SERVICE_ID = getEnv('SERVICE_ID') || ASSISTANT_SLUG;
|
||||
export const CLAUDE_SERVICE_ID = getEnv('CLAUDE_SERVICE_ID') || 'claude';
|
||||
export const CODEX_MAIN_SERVICE_ID =
|
||||
getEnv('CODEX_MAIN_SERVICE_ID') || 'codex-main';
|
||||
export const CODEX_REVIEW_SERVICE_ID =
|
||||
getEnv('CODEX_REVIEW_SERVICE_ID') || 'codex-review';
|
||||
const CONFIG = loadConfig();
|
||||
|
||||
export const ASSISTANT_NAME = CONFIG.assistant.name;
|
||||
export const ASSISTANT_HAS_OWN_NUMBER = CONFIG.assistant.hasOwnNumber;
|
||||
export const SERVICE_ID = CONFIG.service.id;
|
||||
export const CLAUDE_SERVICE_ID = CONFIG.service.claudeId;
|
||||
export const CODEX_MAIN_SERVICE_ID = CONFIG.service.codexMainId;
|
||||
export const CODEX_REVIEW_SERVICE_ID = CONFIG.service.codexReviewId;
|
||||
|
||||
export function normalizeServiceId(serviceId: string): string {
|
||||
if (serviceId === 'codex') {
|
||||
@@ -35,94 +34,52 @@ export function isCodexMainService(serviceId: string = SERVICE_ID): boolean {
|
||||
export function isReviewService(serviceId: string = SERVICE_ID): boolean {
|
||||
return normalizeServiceId(serviceId) === CODEX_REVIEW_SERVICE_ID;
|
||||
}
|
||||
export const POLL_INTERVAL = 2000;
|
||||
export const SCHEDULER_POLL_INTERVAL = 60000;
|
||||
export const POLL_INTERVAL = CONFIG.runtime.pollInterval;
|
||||
export const SCHEDULER_POLL_INTERVAL = CONFIG.runtime.schedulerPollInterval;
|
||||
|
||||
/** Minimum time (ms) a failover lease stays active before Claude can reclaim it. */
|
||||
export const FAILOVER_MIN_DURATION_MS = 3 * 60 * 60 * 1000; // 3 hours
|
||||
export const FAILOVER_MIN_DURATION_MS = CONFIG.runtime.failoverMinDurationMs;
|
||||
|
||||
const PROJECT_ROOT = process.cwd();
|
||||
const HOME_DIR = process.env.HOME || os.homedir();
|
||||
export const SENDER_ALLOWLIST_PATH = CONFIG.paths.senderAllowlistPath;
|
||||
export const STORE_DIR = CONFIG.paths.storeDir;
|
||||
export const GROUPS_DIR = CONFIG.paths.groupsDir;
|
||||
export const DATA_DIR = CONFIG.paths.dataDir;
|
||||
export const CACHE_DIR = CONFIG.paths.cacheDir;
|
||||
|
||||
export const SENDER_ALLOWLIST_PATH = path.join(
|
||||
HOME_DIR,
|
||||
'.config',
|
||||
'ejclaw',
|
||||
'sender-allowlist.json',
|
||||
);
|
||||
export const STORE_DIR = path.resolve(
|
||||
process.env.EJCLAW_STORE_DIR || path.join(PROJECT_ROOT, 'store'),
|
||||
);
|
||||
export const GROUPS_DIR = path.resolve(
|
||||
process.env.EJCLAW_GROUPS_DIR || path.join(PROJECT_ROOT, 'groups'),
|
||||
);
|
||||
export const DATA_DIR = path.resolve(
|
||||
process.env.EJCLAW_DATA_DIR || path.join(PROJECT_ROOT, 'data'),
|
||||
);
|
||||
// Shared cache directory (same across both services for dedup)
|
||||
export const CACHE_DIR = path.join(PROJECT_ROOT, 'cache');
|
||||
|
||||
export const AGENT_TIMEOUT = parseInt(
|
||||
process.env.AGENT_TIMEOUT || '1800000',
|
||||
10,
|
||||
);
|
||||
export const AGENT_MAX_OUTPUT_SIZE = parseInt(
|
||||
process.env.AGENT_MAX_OUTPUT_SIZE || '10485760',
|
||||
10,
|
||||
); // 10MB default
|
||||
export const IPC_POLL_INTERVAL = 1000;
|
||||
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep agent alive after last result
|
||||
export const MAX_CONCURRENT_AGENTS = Math.max(
|
||||
1,
|
||||
parseInt(process.env.MAX_CONCURRENT_AGENTS || '5', 10) || 5,
|
||||
);
|
||||
export const RECOVERY_CONCURRENT_AGENTS = parseInt(
|
||||
getEnv('RECOVERY_CONCURRENT_AGENTS') || '3',
|
||||
10,
|
||||
);
|
||||
export const AGENT_TIMEOUT = CONFIG.runtime.agentTimeout;
|
||||
export const AGENT_MAX_OUTPUT_SIZE = CONFIG.runtime.agentMaxOutputSize;
|
||||
export const IPC_POLL_INTERVAL = CONFIG.runtime.ipcPollInterval;
|
||||
export const IDLE_TIMEOUT = CONFIG.runtime.idleTimeout;
|
||||
export const MAX_CONCURRENT_AGENTS = CONFIG.runtime.maxConcurrentAgents;
|
||||
export const RECOVERY_CONCURRENT_AGENTS =
|
||||
CONFIG.runtime.recoveryConcurrentAgents;
|
||||
export const LOG_LEVEL = CONFIG.logging.level;
|
||||
export const IS_TEST_ENV = CONFIG.logging.isTestEnv;
|
||||
|
||||
// ── Paired review ─────────────────────────────────────────────────
|
||||
|
||||
/** Owner agent type. Default: codex. Set OWNER_AGENT_TYPE=claude-code to use Claude as owner. */
|
||||
const rawOwnerAgentType = getEnv('OWNER_AGENT_TYPE');
|
||||
export const OWNER_AGENT_TYPE: AgentType =
|
||||
rawOwnerAgentType === 'codex' || rawOwnerAgentType === 'claude-code'
|
||||
? rawOwnerAgentType
|
||||
: 'codex';
|
||||
export const OWNER_AGENT_TYPE = CONFIG.paired.ownerAgentType;
|
||||
|
||||
/** Reviewer agent type. Default: claude-code. Set REVIEWER_AGENT_TYPE=codex to use Codex as reviewer. */
|
||||
const rawReviewerAgentType = getEnv('REVIEWER_AGENT_TYPE');
|
||||
export const REVIEWER_AGENT_TYPE: AgentType =
|
||||
rawReviewerAgentType === 'codex' || rawReviewerAgentType === 'claude-code'
|
||||
? rawReviewerAgentType
|
||||
: 'claude-code';
|
||||
export const REVIEWER_AGENT_TYPE = CONFIG.paired.reviewerAgentType;
|
||||
|
||||
/** Service ID for the reviewer based on agent type. */
|
||||
export const REVIEWER_SERVICE_ID_FOR_TYPE =
|
||||
REVIEWER_AGENT_TYPE === 'claude-code'
|
||||
? CLAUDE_SERVICE_ID
|
||||
: CODEX_REVIEW_SERVICE_ID;
|
||||
CONFIG.paired.reviewerServiceIdForType;
|
||||
|
||||
/** Arbiter agent type. Disabled by default. Set ARBITER_AGENT_TYPE=codex or claude-code to enable. */
|
||||
const rawArbiterAgentType = getEnv('ARBITER_AGENT_TYPE');
|
||||
export const ARBITER_AGENT_TYPE: AgentType | undefined =
|
||||
rawArbiterAgentType === 'codex' || rawArbiterAgentType === 'claude-code'
|
||||
? rawArbiterAgentType
|
||||
: undefined;
|
||||
export const ARBITER_AGENT_TYPE = CONFIG.paired.arbiterAgentType;
|
||||
|
||||
/** Service ID for the arbiter. Defaults to codex-review for internal routing when arbiter is enabled. */
|
||||
export const ARBITER_SERVICE_ID = ARBITER_AGENT_TYPE
|
||||
? getEnv('ARBITER_SERVICE_ID') || CODEX_REVIEW_SERVICE_ID
|
||||
: null;
|
||||
export const ARBITER_SERVICE_ID = CONFIG.paired.arbiterServiceId;
|
||||
|
||||
/** Language for agent responses. When set, a language instruction is appended to all paired room prompts. */
|
||||
export const AGENT_LANGUAGE = getEnv('AGENT_LANGUAGE') || '';
|
||||
export const AGENT_LANGUAGE = CONFIG.paired.agentLanguage;
|
||||
|
||||
/** Number of consecutive owner↔reviewer round trips before arbiter is auto-requested. */
|
||||
export const ARBITER_DEADLOCK_THRESHOLD = parseInt(
|
||||
getEnv('ARBITER_DEADLOCK_THRESHOLD') || '2',
|
||||
10,
|
||||
);
|
||||
export const ARBITER_DEADLOCK_THRESHOLD =
|
||||
CONFIG.paired.arbiterDeadlockThreshold;
|
||||
|
||||
export function isArbiterEnabled(): boolean {
|
||||
return ARBITER_AGENT_TYPE !== undefined;
|
||||
@@ -130,26 +87,11 @@ export function isArbiterEnabled(): boolean {
|
||||
|
||||
// ── Per-role model configuration ─────────────────────────────────
|
||||
|
||||
export interface RoleModelConfig {
|
||||
/** Model name override (e.g. 'claude-opus-4-6', 'gpt-5.4'). */
|
||||
model?: string;
|
||||
/** Effort level override. */
|
||||
effort?: string;
|
||||
/** Whether to fall back to codex when primary provider fails. Default: true. */
|
||||
fallbackEnabled: boolean;
|
||||
}
|
||||
|
||||
function buildRoleModelConfig(envPrefix: string): RoleModelConfig {
|
||||
return {
|
||||
model: getEnv(`${envPrefix}_MODEL`) || undefined,
|
||||
effort: getEnv(`${envPrefix}_EFFORT`) || undefined,
|
||||
fallbackEnabled: getEnv(`${envPrefix}_FALLBACK_ENABLED`) !== 'false',
|
||||
};
|
||||
}
|
||||
|
||||
export const OWNER_MODEL_CONFIG = buildRoleModelConfig('OWNER');
|
||||
export const REVIEWER_MODEL_CONFIG = buildRoleModelConfig('REVIEWER');
|
||||
export const ARBITER_MODEL_CONFIG = buildRoleModelConfig('ARBITER');
|
||||
export const OWNER_MODEL_CONFIG = CONFIG.models.owner;
|
||||
export const REVIEWER_MODEL_CONFIG = CONFIG.models.reviewer;
|
||||
export const ARBITER_MODEL_CONFIG = CONFIG.models.arbiter;
|
||||
export const DEFAULT_CLAUDE_MODEL = CONFIG.providers.claudeDefaultModel;
|
||||
export const DEFAULT_CODEX_MODEL = CONFIG.providers.codexDefaultModel;
|
||||
|
||||
export function getRoleModelConfig(
|
||||
role: 'owner' | 'reviewer' | 'arbiter',
|
||||
@@ -166,107 +108,38 @@ export function getRoleModelConfig(
|
||||
|
||||
// ── Mixture of Agents (MoA) ──────────────────────────────────────
|
||||
|
||||
import type { MoaConfig, MoaModelConfig } from './moa.js';
|
||||
|
||||
/**
|
||||
* Parse MOA reference models from env.
|
||||
* Format: MOA_REF_MODELS=kimi,glm (comma-separated names)
|
||||
* Each model: MOA_{NAME}_MODEL, MOA_{NAME}_BASE_URL, MOA_{NAME}_API_KEY
|
||||
*/
|
||||
function parseMoaReferenceModels(): MoaModelConfig[] {
|
||||
const names = (getEnv('MOA_REF_MODELS') || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return names
|
||||
.map((name) => {
|
||||
const prefix = `MOA_${name.toUpperCase()}`;
|
||||
const model = getEnv(`${prefix}_MODEL`) || '';
|
||||
const baseUrl = getEnv(`${prefix}_BASE_URL`) || '';
|
||||
const apiKey = getEnv(`${prefix}_API_KEY`) || '';
|
||||
if (!model || !baseUrl || !apiKey) return null;
|
||||
const rawFormat = getEnv(`${prefix}_API_FORMAT`) || '';
|
||||
const apiFormat: 'openai' | 'anthropic' =
|
||||
rawFormat === 'anthropic' ? 'anthropic' : 'openai';
|
||||
return { name, model, baseUrl, apiKey, apiFormat };
|
||||
})
|
||||
.filter((m): m is MoaModelConfig => m !== null);
|
||||
}
|
||||
|
||||
export function getMoaConfig(): MoaConfig {
|
||||
const referenceModels = parseMoaReferenceModels();
|
||||
return {
|
||||
enabled: getEnv('MOA_ENABLED') === 'true' && referenceModels.length > 0,
|
||||
referenceModels,
|
||||
};
|
||||
return CONFIG.moa;
|
||||
}
|
||||
|
||||
// Max owner↔reviewer round trips per task. 0 = unlimited.
|
||||
const rawMaxRoundTrips = getEnv('PAIRED_MAX_ROUND_TRIPS') || '1000';
|
||||
export const PAIRED_MAX_ROUND_TRIPS =
|
||||
rawMaxRoundTrips === '0' ? Infinity : parseInt(rawMaxRoundTrips, 10) || 1000;
|
||||
export const PAIRED_MAX_ROUND_TRIPS = CONFIG.paired.maxRoundTrips;
|
||||
|
||||
export const RECOVERY_STAGGER_MS = parseInt(
|
||||
getEnv('RECOVERY_STAGGER_MS') || '2000',
|
||||
10,
|
||||
);
|
||||
export const RECOVERY_DURATION_MS = parseInt(
|
||||
getEnv('RECOVERY_DURATION_MS') || '60000',
|
||||
10,
|
||||
);
|
||||
export const RECOVERY_STAGGER_MS = CONFIG.runtime.recoveryStaggerMs;
|
||||
export const RECOVERY_DURATION_MS = CONFIG.runtime.recoveryDurationMs;
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export const TRIGGER_PATTERN = new RegExp(
|
||||
`^@${escapeRegex(ASSISTANT_NAME)}\\b`,
|
||||
'i',
|
||||
);
|
||||
export const TRIGGER_PATTERN = CONFIG.assistant.triggerPattern;
|
||||
|
||||
// Status dashboard: Discord channel ID for live agent status updates
|
||||
export const STATUS_CHANNEL_ID = process.env.STATUS_CHANNEL_ID || '';
|
||||
export const STATUS_UPDATE_INTERVAL = 10000; // 10s
|
||||
export const USAGE_UPDATE_INTERVAL = 300000; // 5 minutes
|
||||
export const STATUS_SHOW_ROOMS =
|
||||
(getEnv('STATUS_SHOW_ROOMS') || 'true') !== 'false';
|
||||
export const STATUS_SHOW_ROOM_DETAILS =
|
||||
(getEnv('STATUS_SHOW_ROOM_DETAILS') || 'true') !== 'false';
|
||||
export const USAGE_DASHBOARD_ENABLED = getEnv('USAGE_DASHBOARD') === 'true';
|
||||
export const STATUS_CHANNEL_ID = CONFIG.status.channelId;
|
||||
export const STATUS_UPDATE_INTERVAL = CONFIG.status.updateInterval;
|
||||
export const USAGE_UPDATE_INTERVAL = CONFIG.status.usageUpdateInterval;
|
||||
export const STATUS_SHOW_ROOMS = CONFIG.status.showRooms;
|
||||
export const STATUS_SHOW_ROOM_DETAILS = CONFIG.status.showRoomDetails;
|
||||
export const USAGE_DASHBOARD_ENABLED = CONFIG.status.usageDashboardEnabled;
|
||||
|
||||
// Timezone for scheduled tasks (cron expressions, etc.)
|
||||
// Uses system timezone by default
|
||||
export const TIMEZONE =
|
||||
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
export const TIMEZONE = CONFIG.status.timezone;
|
||||
|
||||
const rawSessionCommandAllowedSenders =
|
||||
getEnv('SESSION_COMMAND_ALLOWED_SENDERS') ||
|
||||
getEnv('SESSION_COMMAND_USER_IDS') ||
|
||||
'';
|
||||
|
||||
const SESSION_COMMAND_ALLOWED_SENDERS = new Set(
|
||||
rawSessionCommandAllowedSenders
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const SESSION_COMMAND_ALLOWED_SENDERS = CONFIG.sessionCommands.allowedSenders;
|
||||
|
||||
export function isSessionCommandSenderAllowed(sender: string): boolean {
|
||||
return SESSION_COMMAND_ALLOWED_SENDERS.has(sender);
|
||||
}
|
||||
|
||||
// Delta handoff: cross-provider session continuity with probe + fallback
|
||||
export const DELTA_HANDOFF_ENABLED = getEnv('DELTA_HANDOFF_ENABLED') === 'true';
|
||||
|
||||
// Comma-separated list of group folders for canary testing
|
||||
const rawDeltaHandoffCanaryGroups = getEnv('DELTA_HANDOFF_CANARY_GROUPS') || '';
|
||||
export const DELTA_HANDOFF_CANARY_GROUPS = new Set(
|
||||
rawDeltaHandoffCanaryGroups
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
export const DELTA_HANDOFF_ENABLED = CONFIG.deltaHandoff.enabled;
|
||||
export const DELTA_HANDOFF_CANARY_GROUPS = CONFIG.deltaHandoff.canaryGroups;
|
||||
|
||||
export function isDeltaHandoffEnabledForGroup(groupFolder: string): boolean {
|
||||
if (!DELTA_HANDOFF_ENABLED) return false;
|
||||
|
||||
Reference in New Issue
Block a user