Merge origin/main into codex/local-memory-v1
This commit is contained in:
@@ -31,6 +31,10 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
|||||||
zip \
|
zip \
|
||||||
unzip \
|
unzip \
|
||||||
strace \
|
strace \
|
||||||
|
ripgrep \
|
||||||
|
fd-find \
|
||||||
|
bat \
|
||||||
|
tree \
|
||||||
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||||
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
||||||
@@ -52,18 +56,24 @@ RUN curl -fsSL https://bun.sh/install | bash
|
|||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy host's agent-runner (same code, same SDK version)
|
# Copy agent-runner (Claude Code)
|
||||||
COPY runners/agent-runner/package*.json ./
|
COPY runners/agent-runner/package*.json ./agent/
|
||||||
RUN bun install
|
RUN bun install --cwd ./agent
|
||||||
COPY runners/agent-runner/ ./
|
COPY runners/agent-runner/ ./agent/
|
||||||
RUN bun run build
|
RUN bun run --cwd ./agent build
|
||||||
|
|
||||||
|
# Copy codex-runner
|
||||||
|
COPY runners/codex-runner/package*.json ./codex/
|
||||||
|
RUN bun install --cwd ./codex
|
||||||
|
COPY runners/codex-runner/ ./codex/
|
||||||
|
RUN bun run --cwd ./codex build
|
||||||
|
|
||||||
# Create workspace directories
|
# Create workspace directories
|
||||||
RUN mkdir -p /workspace/project /workspace/group /workspace/global \
|
RUN mkdir -p /workspace/project /workspace/group /workspace/global \
|
||||||
/workspace/ipc/messages /workspace/ipc/input
|
/workspace/ipc/messages /workspace/ipc/input
|
||||||
|
|
||||||
# Create entrypoint script
|
# Entrypoint (fallback — persistent containers use docker exec with explicit runner path)
|
||||||
RUN printf '#!/bin/bash\nset -e\ncat > /tmp/input.json\nbun /app/dist/index.js < /tmp/input.json\n' \
|
RUN printf '#!/bin/bash\nset -e\ncat > /tmp/input.json\nbun /app/agent/dist/index.js < /tmp/input.json\n' \
|
||||||
> /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
> /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
# Set ownership to node user (non-root)
|
# Set ownership to node user (non-root)
|
||||||
|
|||||||
106
docs/architecture.md
Normal file
106
docs/architecture.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Service Stack
|
||||||
|
|
||||||
|
Single unified service (`ejclaw`) manages all three Discord bots in one process:
|
||||||
|
|
||||||
|
- `ejclaw.service` — Single unified process
|
||||||
|
- Discord bots: `DISCORD_BOT_TOKEN` (Claude), `DISCORD_CODEX_BOT_TOKEN` (Codex-main), `DISCORD_REVIEW_BOT_TOKEN` (Codex-review)
|
||||||
|
- Paired review: owner ↔ reviewer (agent types configurable per role)
|
||||||
|
- Reviewer fallback: Claude exhaustion → codex-review auto-handoff
|
||||||
|
- Shared dirs: `store/`, `groups/`, `data/`
|
||||||
|
- SQLite WAL mode + `busy_timeout=5000` for concurrent access
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (host process)
|
||||||
|
│ │
|
||||||
|
│ ▼ (auto-trigger)
|
||||||
|
├──► Reviewer (Docker container, :ro mount)
|
||||||
|
│ │
|
||||||
|
│ Verdict routing
|
||||||
|
│ ├─ DONE → change detection → finalize or re-review
|
||||||
|
│ ├─ BLOCKED → Arbiter (if enabled) or User
|
||||||
|
│ └─ Feedback → Owner (loop)
|
||||||
|
│
|
||||||
|
├──► Arbiter (on-demand, fresh session each time)
|
||||||
|
│ │
|
||||||
|
│ ┌───┴─── MoA (if enabled) ───┐
|
||||||
|
│ │ Ref model A ──► opinion │
|
||||||
|
│ │ Ref model B ──► opinion │
|
||||||
|
│ │ → injected into prompt │
|
||||||
|
│ └────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ PROCEED/REVISE/RESET/ESCALATE
|
||||||
|
│
|
||||||
|
IPC polling ◄── follow-up messages
|
||||||
|
│
|
||||||
|
┌────────── Router ──────────┐
|
||||||
|
▼ ▼
|
||||||
|
paired_turn_outputs Discord (display only)
|
||||||
|
(agent ↔ agent data) (user observation, @mention)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Room Assignment Model
|
||||||
|
|
||||||
|
Per-room routing uses an explicit assignment model:
|
||||||
|
|
||||||
|
- `room_settings` is the room-level source of truth (SSOT)
|
||||||
|
- Each room stores:
|
||||||
|
- `room_mode`: `single` or `tribunal`
|
||||||
|
- `owner_agent_type`: `codex` or `claude-code`
|
||||||
|
- Public room assignment uses `assign_room`
|
||||||
|
- `registered_groups` remains as a materialized capability/read-model layer
|
||||||
|
|
||||||
|
Operationally:
|
||||||
|
|
||||||
|
- `single` → one owner bot
|
||||||
|
- `tribunal` → per-room owner + globally configured reviewer + optional arbiter
|
||||||
|
|
||||||
|
Tribunal is no longer inferred from "two bots registered on one room"; it is an explicit room setting.
|
||||||
|
|
||||||
|
## Tribunal 3-Agent System
|
||||||
|
|
||||||
|
```
|
||||||
|
User message
|
||||||
|
→ Owner responds (implementation, answer, etc.)
|
||||||
|
→ Reviewer auto-triggered (critical review, design check)
|
||||||
|
→ Verdict:
|
||||||
|
DONE → Owner finalizes → Task completed → @user ✅
|
||||||
|
DONE_WITH_CONCERNS → Owner addresses feedback → loop
|
||||||
|
BLOCKED/NEEDS_CONTEXT
|
||||||
|
├─ Arbiter enabled → Arbiter judges → PROCEED/REVISE/RESET/ESCALATE
|
||||||
|
└─ Arbiter disabled → Escalate to user → @user ⚠️
|
||||||
|
→ Owner BLOCKED/NEEDS_CONTEXT → Arbiter (same path as reviewer)
|
||||||
|
→ Deadlock (3+ round trips without progress)
|
||||||
|
→ Arbiter summoned → binding verdict → loop resumes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container Isolation
|
||||||
|
|
||||||
|
Reviewer and arbiter run inside a Docker container with:
|
||||||
|
|
||||||
|
- Read-only source mount (kernel-level write protection)
|
||||||
|
- Both Claude (agent-runner) and Codex (codex-runner) supported
|
||||||
|
- Runner selected at `docker exec` time based on agent type
|
||||||
|
- tmpfs overlays for test runner caches
|
||||||
|
- IPC via filesystem for follow-up messages
|
||||||
|
- Credentials injected per-exec (never baked into container)
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/index.ts` | Orchestrator: state, message loop, agent invocation |
|
||||||
|
| `src/agent-runner.ts` | Spawns agent processes, manages env/sessions/skills |
|
||||||
|
| `src/container-runner.ts` | Docker container execution for reviewer/arbiter |
|
||||||
|
| `src/channels/discord.ts` | Discord channel (8s typing refresh, Whisper transcription) |
|
||||||
|
| `src/ipc.ts` | IPC watcher and task processing |
|
||||||
|
| `src/router.ts` | Message formatting and outbound routing |
|
||||||
|
| `src/config.ts` | Trigger pattern, paths, intervals |
|
||||||
|
| `src/task-scheduler.ts` | Runs scheduled tasks |
|
||||||
|
| `src/db.ts` | SQLite operations |
|
||||||
|
| `runners/agent-runner/` | Claude Code runner (Agent SDK) |
|
||||||
|
| `runners/codex-runner/` | Codex runner (SDK, `codex exec` wrapper) |
|
||||||
|
| `groups/{name}/CLAUDE.md` | Per-group memory (isolated) |
|
||||||
83
docs/configuration.md
Normal file
83
docs/configuration.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
All configuration in a single `.env` file.
|
||||||
|
|
||||||
|
## Discord Bots
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DISCORD_BOT_TOKEN= # Claude bot
|
||||||
|
DISCORD_CODEX_BOT_TOKEN= # Codex-main bot (owner)
|
||||||
|
DISCORD_REVIEW_BOT_TOKEN= # Codex-review bot (arbiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Types & Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Agent types per role
|
||||||
|
OWNER_AGENT_TYPE=codex # codex | claude-code
|
||||||
|
REVIEWER_AGENT_TYPE=claude-code # claude-code | codex
|
||||||
|
ARBITER_AGENT_TYPE=codex # codex | claude-code (optional, enables 3rd agent)
|
||||||
|
|
||||||
|
# Per-role model overrides
|
||||||
|
OWNER_MODEL=gpt-5.4
|
||||||
|
REVIEWER_MODEL=claude-opus-4-6
|
||||||
|
ARBITER_MODEL=gpt-5.4
|
||||||
|
|
||||||
|
# Per-role effort level
|
||||||
|
OWNER_EFFORT=xhigh
|
||||||
|
REVIEWER_EFFORT=high
|
||||||
|
ARBITER_EFFORT=xhigh
|
||||||
|
|
||||||
|
# Per-role fallback toggle
|
||||||
|
REVIEWER_FALLBACK_ENABLED=true # Auto-handoff to codex when Claude exhausted
|
||||||
|
ARBITER_FALLBACK_ENABLED=false
|
||||||
|
|
||||||
|
# Response language
|
||||||
|
AGENT_LANGUAGE=Korean # Injected into all agent prompts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLAUDE_CODE_OAUTH_TOKEN= # Claude Code OAuth token
|
||||||
|
CLAUDE_CODE_OAUTH_TOKENS= # Comma-separated for multi-account rotation
|
||||||
|
OPENAI_API_KEY= # Codex API key (if not using OAuth)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Voice Transcription
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GROQ_API_KEY= # Groq whisper-large-v3-turbo (primary)
|
||||||
|
OPENAI_API_KEY= # OpenAI whisper-1 (fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mixture of Agents (MoA)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MOA_ENABLED=true
|
||||||
|
MOA_REF_MODELS=kimi,glm # Configurable reference model list
|
||||||
|
|
||||||
|
# Per-model config (pattern: MOA_{NAME}_{SETTING})
|
||||||
|
MOA_KIMI_MODEL=kimi-k2.5
|
||||||
|
MOA_KIMI_BASE_URL=https://api.kimi.com/coding
|
||||||
|
MOA_KIMI_API_KEY=sk-kimi-xxx
|
||||||
|
MOA_KIMI_API_FORMAT=anthropic
|
||||||
|
|
||||||
|
MOA_GLM_MODEL=glm-5.1
|
||||||
|
MOA_GLM_BASE_URL=https://open.bigmodel.cn/api/anthropic
|
||||||
|
MOA_GLM_API_KEY=xxx
|
||||||
|
MOA_GLM_API_FORMAT=anthropic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Paths
|
||||||
|
|
||||||
|
| Item | Path |
|
||||||
|
|------|------|
|
||||||
|
| **DB** | `store/messages.db` (shared, WAL mode) |
|
||||||
|
| Service log | `journalctl --user -u ejclaw -f` or `logs/ejclaw.log` |
|
||||||
|
| Per-group logs | `groups/{name}/logs/` |
|
||||||
|
| Claude sessions | `data/sessions/{name}/.claude/` |
|
||||||
|
| Codex sessions | `data/sessions/{name}/.codex/` |
|
||||||
|
| Claude platform rules | `prompts/claude-platform.md` |
|
||||||
|
| Codex platform rules | `prompts/codex-platform.md` |
|
||||||
|
| Global memory | `groups/global/CLAUDE.md` |
|
||||||
@@ -22,6 +22,15 @@ You have been summoned because the owner and reviewer reached a deadlock after m
|
|||||||
- The situation cannot be resolved without user input, regardless of technical agreement
|
- The situation cannot be resolved without user input, regardless of technical agreement
|
||||||
- The same NEEDS_CONTEXT or BLOCKED is repeated after a prior PROCEED — this means your PROCEED did not resolve the issue
|
- The same NEEDS_CONTEXT or BLOCKED is repeated after a prior PROCEED — this means your PROCEED did not resolve the issue
|
||||||
|
|
||||||
|
## MoA (Mixture of Agents) Reference Opinions
|
||||||
|
|
||||||
|
You may receive reference opinions from external models appended to your prompt. When present:
|
||||||
|
|
||||||
|
- **Cite them explicitly** in your verdict — e.g., "Reference model A agrees that...", "Reference model B raises a concern about..."
|
||||||
|
- **Cross-reference** their opinions against the owner/reviewer conversation and code evidence
|
||||||
|
- **Resolve conflicts** — if reference opinions disagree with each other or with owner/reviewer, state which view you adopt and why
|
||||||
|
- Do NOT blindly follow reference opinions — they are inputs to your judgment, not authorities
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Base your verdict on evidence (code, test output, logs), not on who said what first
|
- Base your verdict on evidence (code, test output, logs), not on who said what first
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export type AgentTriggerReason =
|
|||||||
| 'session-failure'
|
| 'session-failure'
|
||||||
| 'overloaded'
|
| 'overloaded'
|
||||||
| 'network-error'
|
| 'network-error'
|
||||||
| 'success-null-result';
|
| 'success-null-result'
|
||||||
|
| 'session-failure';
|
||||||
|
|
||||||
export type ClaudeRotationReason = Extract<
|
export type ClaudeRotationReason = Extract<
|
||||||
AgentTriggerReason,
|
AgentTriggerReason,
|
||||||
|
|||||||
@@ -97,6 +97,26 @@ export async function runAgentProcess(
|
|||||||
memoryBriefing: input.memoryBriefing,
|
memoryBriefing: input.memoryBriefing,
|
||||||
role: containerRole,
|
role: containerRole,
|
||||||
});
|
});
|
||||||
|
// For codex: also write AGENTS.md to the reviewer session dir, because
|
||||||
|
// the container's /home/node/.claude always mounts the reviewer session.
|
||||||
|
// Arbiter and reviewer never run simultaneously, so this is safe.
|
||||||
|
if (containerRole === 'arbiter') {
|
||||||
|
const reviewerSessionDir = path.join(
|
||||||
|
path.dirname(sessionDir),
|
||||||
|
`${group.folder}-reviewer`,
|
||||||
|
);
|
||||||
|
const reviewerCodexDir = path.join(reviewerSessionDir, '.codex');
|
||||||
|
if (fs.existsSync(reviewerSessionDir)) {
|
||||||
|
fs.mkdirSync(reviewerCodexDir, { recursive: true });
|
||||||
|
const arbiterAgentsMd = path.join(sessionDir, '.codex', 'AGENTS.md');
|
||||||
|
if (fs.existsSync(arbiterAgentsMd)) {
|
||||||
|
fs.copyFileSync(
|
||||||
|
arbiterAgentsMd,
|
||||||
|
path.join(reviewerCodexDir, 'AGENTS.md'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return runReviewerContainer({
|
return runReviewerContainer({
|
||||||
|
|||||||
@@ -321,6 +321,28 @@ export function buildReviewerMounts(
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Owner session directory: read-only so reviewer can verify runtime state
|
||||||
|
// files (cron state, configs, etc.) that the owner references by absolute path.
|
||||||
|
const ownerSessionDir = path.join(DATA_DIR, 'sessions', group.folder);
|
||||||
|
if (fs.existsSync(ownerSessionDir)) {
|
||||||
|
mounts.push({
|
||||||
|
hostPath: ownerSessionDir,
|
||||||
|
containerPath: ownerSessionDir,
|
||||||
|
readonly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex OAuth: mount host's ~/.codex (read-only) so codex-runner can authenticate
|
||||||
|
const hostCodexHome =
|
||||||
|
process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
||||||
|
if (fs.existsSync(hostCodexHome)) {
|
||||||
|
mounts.push({
|
||||||
|
hostPath: hostCodexHome,
|
||||||
|
containerPath: '/home/node/.codex',
|
||||||
|
readonly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return mounts;
|
return mounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,12 +389,20 @@ function buildCreateArgs(
|
|||||||
args.push('-e', `SENTRY_AUTH_TOKEN=${process.env.SENTRY_AUTH_TOKEN}`);
|
args.push('-e', `SENTRY_AUTH_TOKEN=${process.env.SENTRY_AUTH_TOKEN}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extra env overrides from paired-execution-context
|
// Extra env overrides from paired-execution-context.
|
||||||
|
// Model/effort keys are excluded here — they are injected per-exec so the
|
||||||
|
// correct agent-type-specific values are used (claude vs codex).
|
||||||
|
const perExecKeys = new Set([
|
||||||
|
'CLAUDE_MODEL',
|
||||||
|
'CLAUDE_EFFORT',
|
||||||
|
'CODEX_MODEL',
|
||||||
|
'CODEX_EFFORT',
|
||||||
|
]);
|
||||||
if (envOverrides) {
|
if (envOverrides) {
|
||||||
for (const [key, value] of Object.entries(envOverrides)) {
|
for (const [key, value] of Object.entries(envOverrides)) {
|
||||||
// Already set above — skip duplicates
|
|
||||||
if (key === 'ANTHROPIC_API_KEY' || key === 'CLAUDE_CODE_OAUTH_TOKEN')
|
if (key === 'ANTHROPIC_API_KEY' || key === 'CLAUDE_CODE_OAUTH_TOKEN')
|
||||||
continue;
|
continue;
|
||||||
|
if (perExecKeys.has(key)) continue;
|
||||||
if (key === 'EJCLAW_WORK_DIR') {
|
if (key === 'EJCLAW_WORK_DIR') {
|
||||||
args.push('-e', 'EJCLAW_WORK_DIR=/workspace/project');
|
args.push('-e', 'EJCLAW_WORK_DIR=/workspace/project');
|
||||||
continue;
|
continue;
|
||||||
@@ -469,22 +499,38 @@ export async function runReviewerContainer(args: {
|
|||||||
'';
|
'';
|
||||||
execArgs.push('-e', `CLAUDE_CODE_OAUTH_TOKEN=${oauthToken}`);
|
execArgs.push('-e', `CLAUDE_CODE_OAUTH_TOKEN=${oauthToken}`);
|
||||||
}
|
}
|
||||||
// Inject model/effort overrides at exec-time so per-role config
|
const isCodexAgent = (group.agentType || 'claude-code') === 'codex';
|
||||||
// takes effect even with persistent containers.
|
logger.info(
|
||||||
const modelEnvKeys = [
|
{
|
||||||
'CLAUDE_MODEL',
|
containerName,
|
||||||
'CLAUDE_EFFORT',
|
groupAgentType: group.agentType,
|
||||||
'CODEX_MODEL',
|
isCodexAgent,
|
||||||
'CODEX_EFFORT',
|
runnerPath: isCodexAgent
|
||||||
];
|
? '/app/codex/dist/index.js'
|
||||||
|
: '/app/agent/dist/index.js',
|
||||||
|
},
|
||||||
|
'Container exec runner selection',
|
||||||
|
);
|
||||||
|
// Inject only agent-type-appropriate model/effort overrides per exec.
|
||||||
if (envOverrides) {
|
if (envOverrides) {
|
||||||
|
const modelEnvKeys = isCodexAgent
|
||||||
|
? ['CODEX_MODEL', 'CODEX_EFFORT']
|
||||||
|
: ['CLAUDE_MODEL', 'CLAUDE_EFFORT'];
|
||||||
for (const key of modelEnvKeys) {
|
for (const key of modelEnvKeys) {
|
||||||
if (envOverrides[key]) {
|
if (envOverrides[key]) {
|
||||||
execArgs.push('-e', `${key}=${envOverrides[key]}`);
|
execArgs.push('-e', `${key}=${envOverrides[key]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
execArgs.push(containerName, 'bun', '/app/dist/index.js');
|
if (isCodexAgent) {
|
||||||
|
// Use session-local .codex dir (contains AGENTS.md with role prompts)
|
||||||
|
// instead of the host-mounted ~/.codex (which has owner-only config).
|
||||||
|
execArgs.push('-e', 'CODEX_HOME=/home/node/.claude/.codex');
|
||||||
|
}
|
||||||
|
const runnerPath = isCodexAgent
|
||||||
|
? '/app/codex/dist/index.js'
|
||||||
|
: '/app/agent/dist/index.js';
|
||||||
|
execArgs.push(containerName, 'bun', runnerPath);
|
||||||
|
|
||||||
return new Promise<AgentOutput>((resolve) => {
|
return new Promise<AgentOutput>((resolve) => {
|
||||||
const proc = spawn(CONTAINER_RUNTIME_BIN, execArgs, {
|
const proc = spawn(CONTAINER_RUNTIME_BIN, execArgs, {
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ function buildModelConfigSection(): string {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const failover = getGlobalFailoverInfo();
|
||||||
const lines = ['🤖 *모델 구성*'];
|
const lines = ['🤖 *모델 구성*'];
|
||||||
for (const role of roleConfigs) {
|
for (const role of roleConfigs) {
|
||||||
if (!role.agentType && role.label === 'Arbiter') continue;
|
if (!role.agentType && role.label === 'Arbiter') continue;
|
||||||
@@ -600,8 +601,16 @@ function buildModelConfigSection(): string {
|
|||||||
? process.env.CODEX_MODEL || 'codex'
|
? process.env.CODEX_MODEL || 'codex'
|
||||||
: process.env.CLAUDE_MODEL || 'claude';
|
: process.env.CLAUDE_MODEL || 'claude';
|
||||||
const model = role.model || defaultModel;
|
const model = role.model || defaultModel;
|
||||||
|
|
||||||
|
// Show fallback status for claude-code roles when global failover is active
|
||||||
|
const isFallback = failover.active && type === 'claude-code';
|
||||||
|
if (isFallback) {
|
||||||
|
const fallbackModel = process.env.CODEX_MODEL || 'codex';
|
||||||
|
lines.push(` **${role.label}** — codex \`${fallbackModel}\` (fallback)`);
|
||||||
|
} else {
|
||||||
lines.push(` **${role.label}** — ${type} \`${model}\``);
|
lines.push(` **${role.label}** — ${type} \`${model}\``);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MoA status
|
// MoA status
|
||||||
const moaConfig = getMoaConfig();
|
const moaConfig = getMoaConfig();
|
||||||
@@ -612,8 +621,6 @@ function buildModelConfigSection(): string {
|
|||||||
lines.push(` **MoA** — ${refs}`);
|
lines.push(` **MoA** — ${refs}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global failover
|
|
||||||
const failover = getGlobalFailoverInfo();
|
|
||||||
if (failover.active) {
|
if (failover.active) {
|
||||||
lines.push(` ⚠️ **Failover 활성** — ${failover.reason || '알 수 없음'}`);
|
lines.push(` ⚠️ **Failover 활성** — ${failover.reason || '알 수 없음'}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user