- DONE → merge_ready → owner final turn (commit/push) → completed - DONE_WITH_CONCERNS → active → owner addresses → ping-pong continues - BLOCKED/NEEDS_CONTEXT → completed (escalate to user, stop ping-pong) - Regular feedback → active → ping-pong continues - Owner detects merge_ready status and finalizes autonomously
194 lines
6.9 KiB
TypeScript
194 lines
6.9 KiB
TypeScript
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import type { AgentType } from './types.js';
|
|
import { getEnv } from './env.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();
|
|
const rawServiceAgentType = getEnv('SERVICE_AGENT_TYPE');
|
|
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';
|
|
export const SERVICE_AGENT_TYPE: AgentType =
|
|
rawServiceAgentType === 'codex' || rawServiceAgentType === 'claude-code'
|
|
? rawServiceAgentType
|
|
: ASSISTANT_SLUG === 'codex'
|
|
? 'codex'
|
|
: 'claude-code';
|
|
|
|
export function normalizeServiceId(serviceId: string): string {
|
|
if (serviceId === 'codex') {
|
|
return CODEX_MAIN_SERVICE_ID;
|
|
}
|
|
return serviceId;
|
|
}
|
|
|
|
export const SERVICE_SESSION_SCOPE = normalizeServiceId(SERVICE_ID);
|
|
|
|
export function isClaudeService(serviceId: string = SERVICE_ID): boolean {
|
|
return normalizeServiceId(serviceId) === CLAUDE_SERVICE_ID;
|
|
}
|
|
|
|
export function isCodexMainService(serviceId: string = SERVICE_ID): boolean {
|
|
return normalizeServiceId(serviceId) === CODEX_MAIN_SERVICE_ID;
|
|
}
|
|
|
|
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;
|
|
|
|
/** 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
|
|
|
|
const PROJECT_ROOT = process.cwd();
|
|
const HOME_DIR = process.env.HOME || os.homedir();
|
|
|
|
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,
|
|
);
|
|
|
|
// ── 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';
|
|
|
|
/** 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';
|
|
|
|
/** 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;
|
|
|
|
// Max owner↔reviewer round trips per task. 0 = unlimited.
|
|
const rawMaxRoundTrips = getEnv('PAIRED_MAX_ROUND_TRIPS') || '10';
|
|
export const PAIRED_MAX_ROUND_TRIPS =
|
|
rawMaxRoundTrips === '0' ? Infinity : parseInt(rawMaxRoundTrips, 10) || 10;
|
|
|
|
// ── Container isolation ───────────────────────────────────────────
|
|
export const REVIEWER_CONTAINER_IMAGE =
|
|
getEnv('REVIEWER_CONTAINER_IMAGE') || 'ejclaw-reviewer:latest';
|
|
export const CREDENTIAL_PROXY_PORT = parseInt(
|
|
getEnv('CREDENTIAL_PROXY_PORT') || '3001',
|
|
10,
|
|
);
|
|
export const RECOVERY_STAGGER_MS = parseInt(
|
|
getEnv('RECOVERY_STAGGER_MS') || '2000',
|
|
10,
|
|
);
|
|
export const RECOVERY_DURATION_MS = parseInt(
|
|
getEnv('RECOVERY_DURATION_MS') || '60000',
|
|
10,
|
|
);
|
|
|
|
function escapeRegex(str: string): string {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
export const TRIGGER_PATTERN = new RegExp(
|
|
`^@${escapeRegex(ASSISTANT_NAME)}\\b`,
|
|
'i',
|
|
);
|
|
|
|
// 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';
|
|
|
|
// Timezone for scheduled tasks (cron expressions, etc.)
|
|
// Uses system timezone by default
|
|
export const TIMEZONE =
|
|
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().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),
|
|
);
|
|
|
|
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 function isDeltaHandoffEnabledForGroup(groupFolder: string): boolean {
|
|
if (!DELTA_HANDOFF_ENABLED) return false;
|
|
// If canary groups are specified, only enable for those groups
|
|
if (DELTA_HANDOFF_CANARY_GROUPS.size > 0) {
|
|
return DELTA_HANDOFF_CANARY_GROUPS.has(groupFolder);
|
|
}
|
|
return true;
|
|
}
|